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

This commit is contained in:
lixinyue
2026-01-19 19:05:36 +08:00
95 changed files with 2408 additions and 1833 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

@@ -231,9 +231,9 @@ async def get_emotion_suggestions(
extra={"group_id": request.group_id} extra={"group_id": request.group_id}
) )
return fail( return fail(
BizCode.RESOURCE_NOT_FOUND, BizCode.NOT_FOUND,
"建议缓存不存在或已过期,请调用 /generate_suggestions 接口生成新建议", "建议缓存不存在或已过期,请调用 /generate_suggestions 接口生成新建议",
None ""
) )
api_logger.info( api_logger.info(
@@ -267,7 +267,7 @@ async def generate_emotion_suggestions(
"""生成个性化情绪建议调用LLM并缓存 """生成个性化情绪建议调用LLM并缓存
Args: Args:
request: 包含 group_id、可选的 config_id 和 force_refresh request: 包含 end_user_id
db: 数据库会话 db: 数据库会话
current_user: 当前用户 current_user: 当前用户
@@ -275,47 +275,22 @@ async def generate_emotion_suggestions(
新生成的个性化情绪建议响应 新生成的个性化情绪建议响应
""" """
try: try:
# 验证 config_id如果提供
# 获取终端用户关联的配置
config_id = request.config_id
if config_id is None:
# 如果没有提供 config_id尝试获取用户关联的配置
try:
from app.services.memory_agent_service import (
get_end_user_connected_config,
)
connected_config = get_end_user_connected_config(request.group_id, db)
config_id = connected_config.get("memory_config_id")
except ValueError as e:
return fail(BizCode.INVALID_PARAMETER, "无法获取用户关联的配置", str(e))
else:
# 如果提供了 config_id验证其有效性
from app.services.memory_config_service import MemoryConfigService
try:
config_service = MemoryConfigService(db)
config = config_service.get_config_by_id(config_id)
if not config:
return fail(BizCode.INVALID_PARAMETER, "配置ID无效", f"配置 {config_id} 不存在")
except Exception as e:
return fail(BizCode.INVALID_PARAMETER, "配置ID验证失败", str(e))
api_logger.info( api_logger.info(
f"用户 {current_user.username} 请求生成个性化情绪建议", f"用户 {current_user.username} 请求生成个性化情绪建议",
extra={ extra={
"group_id": request.group_id, "end_user_id": request.end_user_id
"config_id": config_id
} }
) )
# 调用服务层生成建议 # 调用服务层生成建议
data = await emotion_service.generate_emotion_suggestions( data = await emotion_service.generate_emotion_suggestions(
end_user_id=request.group_id, end_user_id=request.end_user_id,
db=db db=db
) )
# 保存到缓存 # 保存到缓存
await emotion_service.save_suggestions_cache( await emotion_service.save_suggestions_cache(
end_user_id=request.group_id, end_user_id=request.end_user_id,
suggestions_data=data, suggestions_data=data,
db=db, db=db,
expires_hours=24 expires_hours=24
@@ -324,7 +299,7 @@ async def generate_emotion_suggestions(
api_logger.info( api_logger.info(
"个性化建议生成成功", "个性化建议生成成功",
extra={ extra={
"group_id": request.group_id, "end_user_id": request.end_user_id,
"suggestions_count": len(data.get("suggestions", [])) "suggestions_count": len(data.get("suggestions", []))
} }
) )
@@ -334,7 +309,7 @@ async def generate_emotion_suggestions(
except Exception as e: except Exception as e:
api_logger.error( api_logger.error(
f"生成个性化建议失败: {str(e)}", f"生成个性化建议失败: {str(e)}",
extra={"group_id": request.group_id}, extra={"end_user_id": request.end_user_id},
exc_info=True exc_info=True
) )
raise HTTPException( raise HTTPException(

View File

@@ -161,9 +161,9 @@ async def get_preference_tags(
if cached_profile is None: if cached_profile is None:
api_logger.info(f"用户 {user_id} 的画像缓存不存在或已过期") api_logger.info(f"用户 {user_id} 的画像缓存不存在或已过期")
return fail( return fail(
BizCode.RESOURCE_NOT_FOUND, BizCode.NOT_FOUND,
"画像缓存不存在或已过期,请调用 /generate_profile 接口生成新画像", "画像缓存不存在或已过期,请调用 /generate_profile 接口生成新画像",
None ""
) )
# Extract preferences from cache # Extract preferences from cache
@@ -232,9 +232,9 @@ async def get_dimension_portrait(
if cached_profile is None: if cached_profile is None:
api_logger.info(f"用户 {user_id} 的画像缓存不存在或已过期") api_logger.info(f"用户 {user_id} 的画像缓存不存在或已过期")
return fail( return fail(
BizCode.RESOURCE_NOT_FOUND, BizCode.NOT_FOUND,
"画像缓存不存在或已过期,请调用 /generate_profile 接口生成新画像", "画像缓存不存在或已过期,请调用 /generate_profile 接口生成新画像",
None ""
) )
# Extract portrait from cache # Extract portrait from cache
@@ -280,9 +280,9 @@ async def get_interest_area_distribution(
if cached_profile is None: if cached_profile is None:
api_logger.info(f"用户 {user_id} 的画像缓存不存在或已过期") api_logger.info(f"用户 {user_id} 的画像缓存不存在或已过期")
return fail( return fail(
BizCode.RESOURCE_NOT_FOUND, BizCode.NOT_FOUND,
"画像缓存不存在或已过期,请调用 /generate_profile 接口生成新画像", "画像缓存不存在或已过期,请调用 /generate_profile 接口生成新画像",
None ""
) )
# Extract interest areas from cache # Extract interest areas from cache
@@ -332,9 +332,9 @@ async def get_behavior_habits(
if cached_profile is None: if cached_profile is None:
api_logger.info(f"用户 {user_id} 的画像缓存不存在或已过期") api_logger.info(f"用户 {user_id} 的画像缓存不存在或已过期")
return fail( return fail(
BizCode.RESOURCE_NOT_FOUND, BizCode.NOT_FOUND,
"画像缓存不存在或已过期,请调用 /generate_profile 接口生成新画像", "画像缓存不存在或已过期,请调用 /generate_profile 接口生成新画像",
None ""
) )
# Extract habits from cache # Extract habits from cache

View File

@@ -8,9 +8,10 @@ from sqlalchemy.orm import Session
from app.core.logging_config import get_business_logger from app.core.logging_config import get_business_logger
from app.core.response_utils import success 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.dependencies import get_share_user_id, ShareTokenData
from app.repositories import knowledge_repository 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 import release_share_schema, conversation_schema
from app.schemas.response_schema import PageData, PageMeta from app.schemas.response_schema import PageData, PageMeta
from app.services import workspace_service 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.release_share_service import ReleaseShareService
from app.services.shared_chat_service import SharedChatService from app.services.shared_chat_service import SharedChatService
from app.services.app_chat_service import AppChatService, get_app_chat_service 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"]) router = APIRouter(prefix="/public/share", tags=["Public Share"])
logger = get_business_logger() logger = get_business_logger()
@@ -65,10 +67,10 @@ def get_or_generate_user_id(payload_user_id: str, request: Request) -> str:
summary="获取访问 token" summary="获取访问 token"
) )
def get_access_token( def get_access_token(
share_token: str, share_token: str,
payload: release_share_schema.TokenRequest, payload: release_share_schema.TokenRequest,
request: Request, request: Request,
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
"""获取访问 token """获取访问 token
@@ -113,9 +115,9 @@ def get_access_token(
response_model=None response_model=None
) )
def get_shared_release( def get_shared_release(
password: str = Query(None, description="访问密码(如果需要)"), password: str = Query(None, description="访问密码(如果需要)"),
share_data: ShareTokenData = Depends(get_share_user_id), share_data: ShareTokenData = Depends(get_share_user_id),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
"""获取公开分享的发布版本信息 """获取公开分享的发布版本信息
@@ -137,9 +139,9 @@ def get_shared_release(
summary="验证访问密码" summary="验证访问密码"
) )
def verify_password( def verify_password(
payload: release_share_schema.PasswordVerifyRequest, payload: release_share_schema.PasswordVerifyRequest,
share_data: ShareTokenData = Depends(get_share_user_id), share_data: ShareTokenData = Depends(get_share_user_id),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
"""验证分享的访问密码 """验证分享的访问密码
@@ -159,11 +161,11 @@ def verify_password(
summary="获取嵌入代码" summary="获取嵌入代码"
) )
def get_embed_code( def get_embed_code(
width: str = Query("100%", description="iframe 宽度"), width: str = Query("100%", description="iframe 宽度"),
height: str = Query("600px", description="iframe 高度"), height: str = Query("600px", description="iframe 高度"),
request: Request = None, request: Request = None,
share_data: ShareTokenData = Depends(get_share_user_id), share_data: ShareTokenData = Depends(get_share_user_id),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
"""获取嵌入代码 """获取嵌入代码
@@ -183,7 +185,6 @@ def get_embed_code(
return success(data=embed_code) return success(data=embed_code)
# ---------- 会话管理接口 ---------- # ---------- 会话管理接口 ----------
@router.get( @router.get(
@@ -191,11 +192,11 @@ def get_embed_code(
summary="获取会话列表" summary="获取会话列表"
) )
def list_conversations( def list_conversations(
password: str = Query(None, description="访问密码"), password: str = Query(None, description="访问密码"),
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
pagesize: int = Query(20, ge=1, le=100), pagesize: int = Query(20, ge=1, le=100),
share_data: ShareTokenData = Depends(get_share_user_id), share_data: ShareTokenData = Depends(get_share_user_id),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
"""获取分享应用的会话列表 """获取分享应用的会话列表
@@ -209,9 +210,9 @@ def list_conversations(
from app.repositories.end_user_repository import EndUserRepository from app.repositories.end_user_repository import EndUserRepository
end_user_repo = EndUserRepository(db) end_user_repo = EndUserRepository(db)
new_end_user = end_user_repo.get_or_create_end_user( new_end_user = end_user_repo.get_or_create_end_user(
app_id=share.app_id, app_id=share.app_id,
other_id=other_id other_id=other_id
) )
logger.debug(new_end_user.id) logger.debug(new_end_user.id)
service = SharedChatService(db) service = SharedChatService(db)
conversations, total = service.list_conversations( conversations, total = service.list_conversations(
@@ -233,10 +234,10 @@ def list_conversations(
summary="获取会话详情(含消息)" summary="获取会话详情(含消息)"
) )
def get_conversation( def get_conversation(
conversation_id: uuid.UUID, conversation_id: uuid.UUID,
password: str = Query(None, description="访问密码"), password: str = Query(None, description="访问密码"),
share_data: ShareTokenData = Depends(get_share_user_id), share_data: ShareTokenData = Depends(get_share_user_id),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
"""获取会话详情和消息历史""" """获取会话详情和消息历史"""
chat_service = SharedChatService(db) chat_service = SharedChatService(db)
@@ -266,10 +267,10 @@ def get_conversation(
summary="发送消息(支持流式和非流式)" summary="发送消息(支持流式和非流式)"
) )
async def chat( async def chat(
payload: conversation_schema.ChatRequest, payload: conversation_schema.ChatRequest,
share_data: ShareTokenData = Depends(get_share_user_id), share_data: ShareTokenData = Depends(get_share_user_id),
db: Session = Depends(get_db), db: Session = Depends(get_db),
app_chat_service: Annotated[AppChatService, Depends(get_app_chat_service)] = None, 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) end_user_id = str(new_end_user.id)
appid=share.app_id appid = share.app_id
"""获取存储类型和工作空间的ID""" """获取存储类型和工作空间的ID"""
# 直接通过 SQLAlchemy 查询 app # 直接通过 SQLAlchemy 查询 app
@@ -425,16 +426,16 @@ async def chat(
# ) # )
async def event_generator(): async def event_generator():
async for event in app_chat_service.agnet_chat_stream( async for event in app_chat_service.agnet_chat_stream(
message=payload.message, message=payload.message,
conversation_id=conversation.id, # 使用已创建的会话 ID conversation_id=conversation.id, # 使用已创建的会话 ID
user_id= str(new_end_user.id), # 转换为字符串 user_id=str(new_end_user.id), # 转换为字符串
variables=payload.variables, variables=payload.variables,
web_search=payload.web_search, web_search=payload.web_search,
config=agent_config, config=agent_config,
memory=payload.memory, memory=payload.memory,
storage_type=storage_type, storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id, user_rag_memory_id=user_rag_memory_id,
workspace_id=workspace_id workspace_id=workspace_id
): ):
yield event yield event
@@ -481,15 +482,15 @@ async def chat(
async def event_generator(): async def event_generator():
async for event in app_chat_service.multi_agent_chat_stream( async for event in app_chat_service.multi_agent_chat_stream(
message=payload.message, message=payload.message,
conversation_id=conversation.id, # 使用已创建的会话 ID conversation_id=conversation.id, # 使用已创建的会话 ID
user_id=str(new_end_user.id), # 转换为字符串 user_id=str(new_end_user.id), # 转换为字符串
variables=payload.variables, variables=payload.variables,
config=config, config=config,
web_search=payload.web_search, web_search=payload.web_search,
memory=payload.memory, memory=payload.memory,
storage_type=storage_type, storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id user_rag_memory_id=user_rag_memory_id
): ):
yield event yield event
@@ -561,24 +562,27 @@ async def chat(
# return success(data=conversation_schema.ChatResponse(**result)) # return success(data=conversation_schema.ChatResponse(**result))
elif app_type == AppType.WORKFLOW: elif app_type == AppType.WORKFLOW:
config = workflow_config_4_app_release(release) 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: if payload.stream:
async def event_generator(): async def event_generator():
async for event in app_chat_service.workflow_chat_stream( async for event in app_chat_service.workflow_chat_stream(
message=payload.message,
message=payload.message, conversation_id=conversation.id, # 使用已创建的会话 ID
conversation_id=conversation.id, # 使用已创建的会话 ID user_id=end_user_id, # 转换为字符串
user_id=end_user_id, # 转换为字符串 variables=payload.variables,
variables=payload.variables, config=config,
config=config, web_search=payload.web_search,
web_search=payload.web_search, memory=payload.memory,
memory=payload.memory, storage_type=storage_type,
storage_type=storage_type, user_rag_memory_id=user_rag_memory_id,
user_rag_memory_id=user_rag_memory_id, app_id=release.app_id,
app_id=release.app_id, workspace_id=workspace_id,
workspace_id=workspace_id release_id=release.id
): ):
event_type = event.get("event", "message") event_type = event.get("event", "message")
event_data = event.get("data", {}) event_data = event.get("data", {})
@@ -610,7 +614,8 @@ async def chat(
storage_type=storage_type, storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id, user_rag_memory_id=user_rag_memory_id,
app_id=release.app_id, app_id=release.app_id,
workspace_id=workspace_id workspace_id=workspace_id,
release_id=release.id
) )
logger.debug( logger.debug(
"工作流试运行返回结果", "工作流试运行返回结果",

View File

@@ -242,8 +242,9 @@ async def chat(
memory=payload.memory, memory=payload.memory,
storage_type=storage_type, storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id, user_rag_memory_id=user_rag_memory_id,
app_id=app.app_id, app_id=app.id,
workspace_id=workspace_id workspace_id=workspace_id,
release_id=app.current_release.id,
): ):
event_type = event.get("event", "message") event_type = event.get("event", "message")
event_data = event.get("data", {}) event_data = event.get("data", {})
@@ -274,8 +275,9 @@ async def chat(
memory=payload.memory, memory=payload.memory,
storage_type=storage_type, storage_type=storage_type,
user_rag_memory_id=user_rag_memory_id, user_rag_memory_id=user_rag_memory_id,
app_id=app.app_id, app_id=app.id,
workspace_id=workspace_id workspace_id=workspace_id,
release_id=app.current_release.id
) )
logger.debug( logger.debug(
"工作流试运行返回结果", "工作流试运行返回结果",

View File

@@ -39,6 +39,7 @@ class Settings:
REDIS_DB: int = int(os.getenv("REDIS_DB", "1")) REDIS_DB: int = int(os.getenv("REDIS_DB", "1"))
REDIS_PASSWORD: str = os.getenv("REDIS_PASSWORD", "") REDIS_PASSWORD: str = os.getenv("REDIS_PASSWORD", "")
# ElasticSearch configuration # ElasticSearch configuration
ELASTICSEARCH_HOST: str = os.getenv("ELASTICSEARCH_HOST", "https://127.0.0.1") ELASTICSEARCH_HOST: str = os.getenv("ELASTICSEARCH_HOST", "https://127.0.0.1")
ELASTICSEARCH_PORT: int = int(os.getenv("ELASTICSEARCH_PORT", "9200")) ELASTICSEARCH_PORT: int = int(os.getenv("ELASTICSEARCH_PORT", "9200"))

View File

@@ -243,6 +243,33 @@ class QWenCV(GptV4):
tmp_path = tmp.name tmp_path = tmp.name
video_path = f"file://{tmp_path}" 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 = [ messages = [
{ {
"role": "user", "role": "user",
@@ -252,7 +279,7 @@ class QWenCV(GptV4):
"fps": 2, "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 from dashscope import MultiModalConversation
audio_path = f"file://{audio_path}" 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 = [ messages = [
{ {
"role": "user", "role": "user",
@@ -68,7 +96,7 @@ class QWenSeq2txt(Base):
"audio": audio_path "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 import uuid
from typing import Any from typing import Any
from langchain_core.runnables import RunnableConfig
from langgraph.graph.state import CompiledStateGraph from langgraph.graph.state import CompiledStateGraph
from app.core.workflow.graph_builder import GraphBuilder from app.core.workflow.graph_builder import GraphBuilder
@@ -53,11 +54,11 @@ class WorkflowExecutor:
self.edges = workflow_config.get("edges", []) self.edges = workflow_config.get("edges", [])
self.execution_config = workflow_config.get("execution_config", {}) self.execution_config = workflow_config.get("execution_config", {})
self.checkpoint_config = { self.checkpoint_config = RunnableConfig(
"configurable": { configurable={
"thread_id": uuid.uuid4(), "thread_id": uuid.uuid4(),
} }
} )
def _prepare_initial_state(self, input_data: dict[str, Any]) -> WorkflowState: def _prepare_initial_state(self, input_data: dict[str, Any]) -> WorkflowState:
"""准备初始状态(注入系统变量和会话变量) """准备初始状态(注入系统变量和会话变量)
@@ -214,13 +215,13 @@ class WorkflowExecutor:
return { return {
"status": "completed", "status": "completed",
"output": final_output, "output": final_output,
"variables": result.get("variables", {}),
"node_outputs": node_outputs, "node_outputs": node_outputs,
"messages": result.get("messages", []), "messages": result.get("messages", []),
"conversation_id": conversation_id, "conversation_id": conversation_id,
"elapsed_time": elapsed_time, "elapsed_time": elapsed_time,
"token_usage": token_usage, "token_usage": token_usage,
"error": result.get("error"), "error": result.get("error"),
"variables": result.get("variables", {}),
} }
def build_graph(self, stream=False) -> CompiledStateGraph: def build_graph(self, stream=False) -> CompiledStateGraph:
@@ -326,11 +327,10 @@ class WorkflowExecutor:
} }
# 1. 构建图 # 1. 构建图
graph = self.build_graph(True) graph = self.build_graph(stream=True)
# 2. 初始化状态(自动注入系统变量) # 2. 初始化状态(自动注入系统变量)
initial_state = self._prepare_initial_state(input_data) initial_state = self._prepare_initial_state(input_data)
# 3. Execute workflow # 3. Execute workflow
try: try:
chunk_count = 0 chunk_count = 0
@@ -346,14 +346,16 @@ class WorkflowExecutor:
mode, data = event mode, data = event
else: else:
# Unexpected format, log and skip # 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 continue
if mode == "custom": if mode == "custom":
# Handle custom streaming events (chunks from nodes via stream writer) # Handle custom streaming events (chunks from nodes via stream writer)
chunk_count += 1 chunk_count += 1
event_type = data.get("type", "node_chunk") # "message" or "node_chunk" 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 { yield {
"event": event_type, # "message" or "node_chunk" "event": event_type, # "message" or "node_chunk"
"data": { "data": {
@@ -380,7 +382,8 @@ class WorkflowExecutor:
variables_sys = variables.get("sys", {}) variables_sys = variables.get("sys", {})
conversation_id = input_data.get("conversation_id") conversation_id = input_data.get("conversation_id")
execution_id = variables_sys.get("execution_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 { yield {
"event": "node_start", "event": "node_start",
@@ -399,7 +402,8 @@ class WorkflowExecutor:
variables_sys = variables.get("sys", {}) variables_sys = variables.get("sys", {})
conversation_id = input_data.get("conversation_id") conversation_id = input_data.get("conversation_id")
execution_id = variables_sys.get("execution_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 { yield {
"event": "node_end", "event": "node_end",
@@ -407,13 +411,15 @@ class WorkflowExecutor:
"node_id": node_name, "node_id": node_name,
"conversation_id": conversation_id, "conversation_id": conversation_id,
"execution_id": execution_id, "execution_id": execution_id,
"timestamp": data.get("timestamp") "timestamp": data.get("timestamp"),
"state": result.get("node_outputs", {}).get(node_name),
} }
} }
elif mode == "updates": elif mode == "updates":
# Handle state updates - store final state # 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() end_time = datetime.datetime.now()
@@ -421,7 +427,7 @@ class WorkflowExecutor:
result = graph.get_state(self.checkpoint_config).values result = graph.get_state(self.checkpoint_config).values
logger.info( logger.info(
f"Workflow execution completed (streaming), " 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 事件 # 发送 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 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 使用情况 """聚合所有节点的 token 使用情况
Args: 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. The state object passed between nodes in a workflow, containing messages, variables, node outputs, etc.
""" """
# List of messages (append mode) # 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 # Set of loop node IDs, used for assigning values in loop nodes
cycle_nodes: list cycle_nodes: list

View File

@@ -21,6 +21,7 @@ class IterationRuntime:
optional parallel execution, flattening of output, and loop control via optional parallel execution, flattening of output, and loop control via
the workflow state. the workflow state.
""" """
def __init__( def __init__(
self, self,
graph: CompiledStateGraph, graph: CompiledStateGraph,
@@ -87,6 +88,7 @@ class IterationRuntime:
self.result.append(output) self.result.append(output)
if not result["looping"]: if not result["looping"]:
self.looping = False self.looping = False
return result
def _create_iteration_tasks(self, array_obj, idx): def _create_iteration_tasks(self, array_obj, idx):
""" """
@@ -124,7 +126,7 @@ class IterationRuntime:
array_obj = VariablePool(self.state).get(input_expression) array_obj = VariablePool(self.state).get(input_expression)
if not isinstance(array_obj, list): if not isinstance(array_obj, list):
raise RuntimeError("Cannot iterate over a non-list variable") raise RuntimeError("Cannot iterate over a non-list variable")
child_state = []
idx = 0 idx = 0
if self.typed_config.parallel: if self.typed_config.parallel:
# Execute iterations in parallel batches # Execute iterations in parallel batches
@@ -132,15 +134,14 @@ class IterationRuntime:
tasks = self._create_iteration_tasks(array_obj, idx) tasks = self._create_iteration_tasks(array_obj, idx)
logger.info(f"Iteration node {self.node_id}: running, concurrency {len(tasks)}") logger.info(f"Iteration node {self.node_id}: running, concurrency {len(tasks)}")
idx += self.typed_config.parallel_count idx += self.typed_config.parallel_count
await asyncio.gather(*tasks) child_state.extend(await asyncio.gather(*tasks))
logger.info(f"Iteration node {self.node_id}: execution completed")
return self.result
else: else:
# Execute iterations sequentially # Execute iterations sequentially
while idx < len(array_obj) and self.looping: while idx < len(array_obj) and self.looping:
logger.info(f"Iteration node {self.node_id}: running") logger.info(f"Iteration node {self.node_id}: running")
item = array_obj[idx] item = array_obj[idx]
result = await self.graph.ainvoke(self._init_iteration_state(item, idx)) result = await self.graph.ainvoke(self._init_iteration_state(item, idx))
child_state.append(result)
output = VariablePool(result).get(self.output_value) output = VariablePool(result).get(self.output_value)
if isinstance(output, list) and self.typed_config.flatten: if isinstance(output, list) and self.typed_config.flatten:
self.result.extend(output) self.result.extend(output)
@@ -150,5 +151,8 @@ class IterationRuntime:
self.looping = False self.looping = False
idx += 1 idx += 1
logger.info(f"Iteration node {self.node_id}: execution completed") logger.info(f"Iteration node {self.node_id}: execution completed")
return self.result return {
"output": self.result,
"__child_state": child_state
}

View File

@@ -67,7 +67,9 @@ class LoopRuntime:
variables=pool.get_all_conversation_vars(), variables=pool.get_all_conversation_vars(),
node_outputs=pool.get_all_node_outputs(), node_outputs=pool.get_all_node_outputs(),
system_vars=pool.get_all_system_vars(), 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 for variable in self.typed_config.cycle_vars
} }
self.state["node_outputs"][self.node_id] = { self.state["node_outputs"][self.node_id] = {
@@ -76,7 +78,9 @@ class LoopRuntime:
variables=pool.get_all_conversation_vars(), variables=pool.get_all_conversation_vars(),
node_outputs=pool.get_all_node_outputs(), node_outputs=pool.get_all_node_outputs(),
system_vars=pool.get_all_system_vars(), 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 for variable in self.typed_config.cycle_vars
} }
loopstate = WorkflowState( loopstate = WorkflowState(
@@ -171,10 +175,11 @@ class LoopRuntime:
""" """
loopstate = self._init_loop_state() loopstate = self._init_loop_state()
loop_time = self.typed_config.max_loop loop_time = self.typed_config.max_loop
child_state = []
while self.evaluate_conditional(loopstate) and loopstate["looping"] and loop_time > 0: while self.evaluate_conditional(loopstate) and loopstate["looping"] and loop_time > 0:
logger.info(f"loop node {self.node_id}: running") 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 loop_time -= 1
logger.info(f"loop node {self.node_id}: execution completed") 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.core.workflow.nodes.knowledge import KnowledgeRetrievalNodeConfig
from app.db import get_db_read from app.db import get_db_read
from app.models import knowledge_model, knowledgeshare_model, ModelType 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.schemas.chunk_schema import RetrieveType
from app.services import knowledge_service, knowledgeshare_service
from app.services.model_service import ModelConfigService from app.services.model_service import ModelConfigService
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -96,7 +95,7 @@ class KnowledgeRetrievalNode(BaseNode):
filters = self._build_kb_filter(kb_ids, knowledge_model.PermissionType.Share) 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, db=db,
filters=filters filters=filters
) )
@@ -105,7 +104,7 @@ class KnowledgeRetrievalNode(BaseNode):
filters = [ filters = [
knowledgeshare_model.KnowledgeShare.target_kb_id.in_(kb_ids) 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, db=db,
filters=filters filters=filters
) )

View File

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

View File

@@ -85,6 +85,7 @@ class LLMNode(BaseNode):
""" """
# 1. 处理消息格式(优先使用 messages # 1. 处理消息格式(优先使用 messages
self.typed_config = LLMNodeConfig(**self.config)
messages_config = self.typed_config.messages messages_config = self.typed_config.messages
if messages_config: if messages_config:
@@ -167,7 +168,7 @@ class LLMNode(BaseNode):
Returns: Returns:
LLM 响应消息 LLM 响应消息
""" """
self.typed_config = LLMNodeConfig(**self.config) # self.typed_config = LLMNodeConfig(**self.config)
llm, prompt_or_messages = self._prepare_llm(state, True) llm, prompt_or_messages = self._prepare_llm(state, True)
logger.info(f"节点 {self.node_id} 开始执行 LLM 调用(非流式)") logger.info(f"节点 {self.node_id} 开始执行 LLM 调用(非流式)")
@@ -269,12 +270,16 @@ class LLMNode(BaseNode):
chunk_count = 0 chunk_count = 0
# 调用 LLM流式支持字符串或消息列表 # 调用 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'): if hasattr(chunk, 'content'):
content = chunk.content content = chunk.content
else: else:
content = str(chunk) content = str(chunk)
if hasattr(chunk, 'response_metadata'):
if chunk.response_metadata:
last_meta_data = chunk.response_metadata
# 只有当内容不为空时才处理 # 只有当内容不为空时才处理
if content: if content:
@@ -288,13 +293,10 @@ class LLMNode(BaseNode):
logger.info(f"节点 {self.node_id} LLM 调用完成,输出长度: {len(full_response)}, 总 chunks: {chunk_count}") logger.info(f"节点 {self.node_id} LLM 调用完成,输出长度: {len(full_response)}, 总 chunks: {chunk_count}")
# 构建完整的 AIMessage包含元数据 # 构建完整的 AIMessage包含元数据
if isinstance(last_chunk, AIMessage): final_message = AIMessage(
final_message = AIMessage( content=full_response,
content=full_response, response_metadata=last_meta_data
response_metadata=last_chunk.response_metadata if hasattr(last_chunk, 'response_metadata') else {} )
)
else:
final_message = AIMessage(content=full_response)
# yield 完成标记 # yield 完成标记
yield {"__final__": True, "result": final_message} yield {"__final__": True, "result": final_message}

View File

@@ -27,8 +27,6 @@ from .tool_model import (
ToolExecution, ToolType, ToolStatus, AuthType, ExecutionStatus ToolExecution, ToolType, ToolStatus, AuthType, ExecutionStatus
) )
from .memory_perceptual_model import MemoryPerceptualModel from .memory_perceptual_model import MemoryPerceptualModel
from .emotion_suggestions_cache_model import EmotionSuggestionsCache
from .implicit_memory_cache_model import ImplicitMemoryCache
__all__ = [ __all__ = [
"Tenants", "Tenants",
@@ -79,6 +77,4 @@ __all__ = [
"AuthType", "AuthType",
"ExecutionStatus", "ExecutionStatus",
"MemoryPerceptualModel", "MemoryPerceptualModel",
"EmotionSuggestionsCache",
"ImplicitMemoryCache"
] ]

View File

@@ -1,24 +0,0 @@
"""情绪建议缓存模型"""
import uuid
import datetime
from sqlalchemy import Column, String, Text, Integer, DateTime, JSON
from sqlalchemy.dialects.postgresql import UUID
from app.db import Base
class EmotionSuggestionsCache(Base):
"""情绪建议缓存表
用于缓存个性化情绪建议,减少 LLM 调用成本,提升响应速度。
"""
__tablename__ = "emotion_suggestions_cache"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
end_user_id = Column(String(255), nullable=False, unique=True, index=True, comment="终端用户ID组ID")
health_summary = Column(Text, nullable=False, comment="健康状态摘要")
suggestions = Column(JSON, nullable=False, comment="建议列表JSON格式")
generated_at = Column(DateTime, nullable=False, default=datetime.datetime.now, comment="生成时间")
expires_at = Column(DateTime, nullable=True, comment="过期时间")
created_at = Column(DateTime, default=datetime.datetime.now)
updated_at = Column(DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now)

View File

@@ -1,27 +0,0 @@
"""隐性记忆缓存模型"""
import uuid
import datetime
from sqlalchemy import Column, String, Integer, DateTime, JSON
from sqlalchemy.dialects.postgresql import UUID
from app.db import Base
class ImplicitMemoryCache(Base):
"""隐性记忆缓存表
用于缓存用户的完整隐性记忆画像,包括偏好标签、四维画像、兴趣领域和行为习惯。
减少 LLM 调用成本,提升响应速度。
"""
__tablename__ = "implicit_memory_cache"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
end_user_id = Column(String(255), nullable=False, unique=True, index=True, comment="终端用户ID")
preferences = Column(JSON, nullable=False, comment="偏好标签列表JSON格式")
portrait = Column(JSON, nullable=False, comment="四维画像对象JSON格式")
interest_areas = Column(JSON, nullable=False, comment="兴趣领域分布对象JSON格式")
habits = Column(JSON, nullable=False, comment="行为习惯列表JSON格式")
generated_at = Column(DateTime, nullable=False, default=datetime.datetime.now, comment="生成时间")
expires_at = Column(DateTime, nullable=True, comment="过期时间")
created_at = Column(DateTime, default=datetime.datetime.now)
updated_at = Column(DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now)

View File

@@ -75,6 +75,14 @@ class WorkflowExecution(Base):
nullable=False, nullable=False,
index=True index=True
) )
release_id = Column(
UUID(as_uuid=True),
ForeignKey("app_releases.id", ondelete="CASCADE"),
nullable=True,
index=True
)
app_id = Column( app_id = Column(
UUID(as_uuid=True), UUID(as_uuid=True),
ForeignKey("apps.id", ondelete="CASCADE"), ForeignKey("apps.id", ondelete="CASCADE"),

View File

@@ -1,163 +0,0 @@
"""情绪建议缓存仓储层"""
from sqlalchemy.orm import Session
from typing import Optional, Dict, Any
import datetime
from app.models.emotion_suggestions_cache_model import EmotionSuggestionsCache
from app.core.logging_config import get_db_logger
# 获取数据库专用日志器
db_logger = get_db_logger()
class EmotionSuggestionsCacheRepository:
"""情绪建议缓存仓储类"""
def __init__(self, db: Session):
self.db = db
def get_by_end_user_id(self, end_user_id: str) -> Optional[EmotionSuggestionsCache]:
"""根据终端用户ID获取缓存
Args:
end_user_id: 终端用户ID组ID
Returns:
缓存记录,如果不存在返回 None
"""
try:
cache = (
self.db.query(EmotionSuggestionsCache)
.filter(EmotionSuggestionsCache.end_user_id == end_user_id)
.first()
)
if cache:
db_logger.info(f"成功获取用户 {end_user_id} 的情绪建议缓存")
else:
db_logger.info(f"用户 {end_user_id} 的情绪建议缓存不存在")
return cache
except Exception as e:
db_logger.error(f"获取用户 {end_user_id} 的情绪建议缓存失败: {str(e)}")
raise
def create_or_update(
self,
end_user_id: str,
health_summary: str,
suggestions: list,
expires_hours: int = 24
) -> EmotionSuggestionsCache:
"""创建或更新缓存
Args:
end_user_id: 终端用户ID组ID
health_summary: 健康状态摘要
suggestions: 建议列表
expires_hours: 过期时间小时默认24小时
Returns:
缓存记录
"""
try:
# 查找现有记录
cache = self.get_by_end_user_id(end_user_id)
now = datetime.datetime.now()
expires_at = now + datetime.timedelta(hours=expires_hours)
if cache:
# 更新现有记录
cache.health_summary = health_summary
cache.suggestions = suggestions
cache.generated_at = now
cache.expires_at = expires_at
cache.updated_at = now
db_logger.info(f"更新用户 {end_user_id} 的情绪建议缓存")
else:
# 创建新记录
cache = EmotionSuggestionsCache(
end_user_id=end_user_id,
health_summary=health_summary,
suggestions=suggestions,
generated_at=now,
expires_at=expires_at,
created_at=now,
updated_at=now
)
self.db.add(cache)
db_logger.info(f"创建用户 {end_user_id} 的情绪建议缓存")
self.db.commit()
self.db.refresh(cache)
return cache
except Exception as e:
self.db.rollback()
db_logger.error(f"创建或更新用户 {end_user_id} 的情绪建议缓存失败: {str(e)}")
raise
def delete_by_end_user_id(self, end_user_id: str) -> bool:
"""删除缓存
Args:
end_user_id: 终端用户ID组ID
Returns:
是否删除成功
"""
try:
cache = self.get_by_end_user_id(end_user_id)
if cache:
self.db.delete(cache)
self.db.commit()
db_logger.info(f"删除用户 {end_user_id} 的情绪建议缓存")
return True
return False
except Exception as e:
self.db.rollback()
db_logger.error(f"删除用户 {end_user_id} 的情绪建议缓存失败: {str(e)}")
raise
@staticmethod
def is_expired(cache: EmotionSuggestionsCache) -> bool:
"""检查缓存是否过期
Args:
cache: 缓存记录
Returns:
是否过期
"""
if cache.expires_at is None:
return False
return datetime.datetime.now() > cache.expires_at
# 便捷函数
def get_cache_by_end_user_id(db: Session, end_user_id: str) -> Optional[EmotionSuggestionsCache]:
"""根据终端用户ID获取缓存"""
repo = EmotionSuggestionsCacheRepository(db)
return repo.get_by_end_user_id(end_user_id)
def create_or_update_cache(
db: Session,
end_user_id: str,
health_summary: str,
suggestions: list,
expires_hours: int = 24
) -> EmotionSuggestionsCache:
"""创建或更新缓存"""
repo = EmotionSuggestionsCacheRepository(db)
return repo.create_or_update(end_user_id, health_summary, suggestions, expires_hours)
def delete_cache_by_end_user_id(db: Session, end_user_id: str) -> bool:
"""删除缓存"""
repo = EmotionSuggestionsCacheRepository(db)
return repo.delete_by_end_user_id(end_user_id)
def is_cache_expired(cache: EmotionSuggestionsCache) -> bool:
"""检查缓存是否过期"""
return EmotionSuggestionsCacheRepository.is_expired(cache)

View File

@@ -1,175 +0,0 @@
"""隐性记忆缓存仓储层"""
from sqlalchemy.orm import Session
from typing import Optional, Dict, Any
import datetime
from app.models.implicit_memory_cache_model import ImplicitMemoryCache
from app.core.logging_config import get_db_logger
# 获取数据库专用日志器
db_logger = get_db_logger()
class ImplicitMemoryCacheRepository:
"""隐性记忆缓存仓储类"""
def __init__(self, db: Session):
self.db = db
def get_by_end_user_id(self, end_user_id: str) -> Optional[ImplicitMemoryCache]:
"""根据终端用户ID获取缓存
Args:
end_user_id: 终端用户ID
Returns:
缓存记录,如果不存在返回 None
"""
try:
cache = (
self.db.query(ImplicitMemoryCache)
.filter(ImplicitMemoryCache.end_user_id == end_user_id)
.first()
)
if cache:
db_logger.info(f"成功获取用户 {end_user_id} 的隐性记忆缓存")
else:
db_logger.info(f"用户 {end_user_id} 的隐性记忆缓存不存在")
return cache
except Exception as e:
db_logger.error(f"获取用户 {end_user_id} 的隐性记忆缓存失败: {str(e)}")
raise
def create_or_update(
self,
end_user_id: str,
preferences: list,
portrait: dict,
interest_areas: dict,
habits: list,
expires_hours: int = 168 # 默认7天
) -> ImplicitMemoryCache:
"""创建或更新缓存
Args:
end_user_id: 终端用户ID
preferences: 偏好标签列表
portrait: 四维画像对象
interest_areas: 兴趣领域分布对象
habits: 行为习惯列表
expires_hours: 过期时间小时默认168小时7天
Returns:
缓存记录
"""
try:
# 查找现有记录
cache = self.get_by_end_user_id(end_user_id)
now = datetime.datetime.now()
expires_at = now + datetime.timedelta(hours=expires_hours)
if cache:
# 更新现有记录
cache.preferences = preferences
cache.portrait = portrait
cache.interest_areas = interest_areas
cache.habits = habits
cache.generated_at = now
cache.expires_at = expires_at
cache.updated_at = now
db_logger.info(f"更新用户 {end_user_id} 的隐性记忆缓存")
else:
# 创建新记录
cache = ImplicitMemoryCache(
end_user_id=end_user_id,
preferences=preferences,
portrait=portrait,
interest_areas=interest_areas,
habits=habits,
generated_at=now,
expires_at=expires_at,
created_at=now,
updated_at=now
)
self.db.add(cache)
db_logger.info(f"创建用户 {end_user_id} 的隐性记忆缓存")
self.db.commit()
self.db.refresh(cache)
return cache
except Exception as e:
self.db.rollback()
db_logger.error(f"创建或更新用户 {end_user_id} 的隐性记忆缓存失败: {str(e)}")
raise
def delete_by_end_user_id(self, end_user_id: str) -> bool:
"""删除缓存
Args:
end_user_id: 终端用户ID
Returns:
是否删除成功
"""
try:
cache = self.get_by_end_user_id(end_user_id)
if cache:
self.db.delete(cache)
self.db.commit()
db_logger.info(f"删除用户 {end_user_id} 的隐性记忆缓存")
return True
return False
except Exception as e:
self.db.rollback()
db_logger.error(f"删除用户 {end_user_id} 的隐性记忆缓存失败: {str(e)}")
raise
@staticmethod
def is_expired(cache: ImplicitMemoryCache) -> bool:
"""检查缓存是否过期
Args:
cache: 缓存记录
Returns:
是否过期
"""
if cache.expires_at is None:
return False
return datetime.datetime.now() > cache.expires_at
# 便捷函数
def get_cache_by_end_user_id(db: Session, end_user_id: str) -> Optional[ImplicitMemoryCache]:
"""根据终端用户ID获取缓存"""
repo = ImplicitMemoryCacheRepository(db)
return repo.get_by_end_user_id(end_user_id)
def create_or_update_cache(
db: Session,
end_user_id: str,
preferences: list,
portrait: dict,
interest_areas: dict,
habits: list,
expires_hours: int = 168
) -> ImplicitMemoryCache:
"""创建或更新缓存"""
repo = ImplicitMemoryCacheRepository(db)
return repo.create_or_update(
end_user_id, preferences, portrait, interest_areas, habits, expires_hours
)
def delete_cache_by_end_user_id(db: Session, end_user_id: str) -> bool:
"""删除缓存"""
repo = ImplicitMemoryCacheRepository(db)
return repo.delete_by_end_user_id(end_user_id)
def is_cache_expired(cache: ImplicitMemoryCache) -> bool:
"""检查缓存是否过期"""
return ImplicitMemoryCacheRepository.is_expired(cache)

View File

@@ -34,5 +34,4 @@ class EmotionSuggestionsRequest(BaseModel):
class EmotionGenerateSuggestionsRequest(BaseModel): class EmotionGenerateSuggestionsRequest(BaseModel):
"""生成个性化情绪建议请求""" """生成个性化情绪建议请求"""
group_id: str = Field(..., description="ID") end_user_id: str = Field(..., description="终端用户ID")
config_id: Optional[int] = Field(None, description="配置ID用于指定LLM模型")

View File

@@ -527,6 +527,7 @@ class AppChatService:
conversation_id: uuid.UUID, conversation_id: uuid.UUID,
config: WorkflowConfig, config: WorkflowConfig,
app_id: uuid.UUID, app_id: uuid.UUID,
release_id: uuid.UUID,
workspace_id: uuid.UUID, workspace_id: uuid.UUID,
user_id: Optional[str] = None, user_id: Optional[str] = None,
variables: Optional[Dict[str, Any]] = None, variables: Optional[Dict[str, Any]] = None,
@@ -549,6 +550,7 @@ class AppChatService:
payload=payload, payload=payload,
config=config, config=config,
workspace_id=workspace_id, workspace_id=workspace_id,
release_id=release_id,
) )
async def workflow_chat_stream( async def workflow_chat_stream(
@@ -557,6 +559,7 @@ class AppChatService:
conversation_id: uuid.UUID, conversation_id: uuid.UUID,
config: WorkflowConfig, config: WorkflowConfig,
app_id: uuid.UUID, app_id: uuid.UUID,
release_id: uuid.UUID,
workspace_id: uuid.UUID, workspace_id: uuid.UUID,
user_id: str = None, user_id: str = None,
variables: Optional[Dict[str, Any]] = None, variables: Optional[Dict[str, Any]] = None,
@@ -565,7 +568,7 @@ class AppChatService:
storage_type: Optional[str] = None, storage_type: Optional[str] = None,
user_rag_memory_id: Optional[str] = None, user_rag_memory_id: Optional[str] = None,
) -> AsyncGenerator[str, None]: ) -> AsyncGenerator[dict, None]:
"""聊天(流式)""" """聊天(流式)"""
workflow_service = WorkflowService(self.db) workflow_service = WorkflowService(self.db)
payload = DraftRunRequest( payload = DraftRunRequest(
@@ -580,6 +583,7 @@ class AppChatService:
payload=payload, payload=payload,
config=config, config=config,
workspace_id=workspace_id, workspace_id=workspace_id,
release_id=release_id
): ):
yield event yield event

View File

@@ -129,7 +129,7 @@ class AppService:
Raises: Raises:
ResourceNotFoundException: 当应用不存在时 ResourceNotFoundException: 当应用不存在时
""" """
app = get_apps_by_id(self.db,app_id) app = get_apps_by_id(self.db, app_id)
if not app: if not app:
logger.warning("应用不存在", extra={"app_id": str(app_id)}) logger.warning("应用不存在", extra={"app_id": str(app_id)})
raise ResourceNotFoundException("应用", str(app_id)) raise ResourceNotFoundException("应用", str(app_id))
@@ -227,7 +227,6 @@ class AppService:
if not model_api_key: if not model_api_key:
raise ResourceNotFoundException("模型配置", str(multi_agent_config.default_model_config_id)) raise ResourceNotFoundException("模型配置", str(multi_agent_config.default_model_config_id))
# 3. 检查子 Agent 配置 # 3. 检查子 Agent 配置
if not multi_agent_config.sub_agents or len(multi_agent_config.sub_agents) == 0: if not multi_agent_config.sub_agents or len(multi_agent_config.sub_agents) == 0:
raise BusinessException( raise BusinessException(
@@ -281,10 +280,10 @@ class AppService:
) )
def _create_agent_config( def _create_agent_config(
self, self,
app_id: uuid.UUID, app_id: uuid.UUID,
config_data: app_schema.AgentConfigCreate, config_data: app_schema.AgentConfigCreate,
now: datetime.datetime now: datetime.datetime
) -> None: ) -> None:
"""创建 Agent 配置(内部方法) """创建 Agent 配置(内部方法)
@@ -313,10 +312,10 @@ class AppService:
logger.debug("Agent 配置已创建", extra={"app_id": str(app_id)}) logger.debug("Agent 配置已创建", extra={"app_id": str(app_id)})
def _create_multi_agent_config( def _create_multi_agent_config(
self, self,
app_id: uuid.UUID, app_id: uuid.UUID,
config_data: Dict[str, Any], config_data: Dict[str, Any],
now: datetime.datetime now: datetime.datetime
) -> None: ) -> None:
"""创建多 Agent 配置(内部方法) """创建多 Agent 配置(内部方法)
@@ -411,9 +410,9 @@ class AppService:
return 1 if max_ver is None else int(max_ver) + 1 return 1 if max_ver is None else int(max_ver) + 1
def _convert_to_schema( def _convert_to_schema(
self, self,
app: App, app: App,
current_workspace_id: uuid.UUID current_workspace_id: uuid.UUID
) -> app_schema.App: ) -> app_schema.App:
"""将 App 模型转换为 Schema并设置 is_shared 字段 """将 App 模型转换为 Schema并设置 is_shared 字段
@@ -447,9 +446,9 @@ class AppService:
# ==================== 应用管理 ==================== # ==================== 应用管理 ====================
def get_app( def get_app(
self, self,
app_id: uuid.UUID, app_id: uuid.UUID,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
) -> App: ) -> App:
"""获取应用详情 """获取应用详情
@@ -469,11 +468,11 @@ class AppService:
return app return app
def create_app( def create_app(
self, self,
*, *,
user_id: uuid.UUID, user_id: uuid.UUID,
workspace_id: uuid.UUID, workspace_id: uuid.UUID,
data: app_schema.AppCreate data: app_schema.AppCreate
) -> App: ) -> App:
"""创建应用 """创建应用
@@ -535,11 +534,11 @@ class AppService:
raise BusinessException(f"应用创建失败: {str(e)}", BizCode.INTERNAL_ERROR, cause=e) raise BusinessException(f"应用创建失败: {str(e)}", BizCode.INTERNAL_ERROR, cause=e)
def update_app( def update_app(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
data: app_schema.AppUpdate, data: app_schema.AppUpdate,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
) -> App: ) -> App:
"""更新应用基本信息 """更新应用基本信息
@@ -578,10 +577,10 @@ class AppService:
return app return app
def delete_app( def delete_app(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
) -> None: ) -> None:
"""删除应用 """删除应用
@@ -612,12 +611,12 @@ class AppService:
) )
def copy_app( def copy_app(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
user_id: uuid.UUID, user_id: uuid.UUID,
workspace_id: Optional[uuid.UUID] = None, workspace_id: Optional[uuid.UUID] = None,
new_name: Optional[str] = None new_name: Optional[str] = None
) -> App: ) -> App:
"""复制应用(包括基础信息和配置) """复制应用(包括基础信息和配置)
@@ -716,16 +715,16 @@ class AppService:
raise BusinessException(f"应用复制失败: {str(e)}", BizCode.INTERNAL_ERROR, cause=e) raise BusinessException(f"应用复制失败: {str(e)}", BizCode.INTERNAL_ERROR, cause=e)
def list_apps( def list_apps(
self, self,
*, *,
workspace_id: uuid.UUID, workspace_id: uuid.UUID,
type: Optional[str] = None, type: Optional[str] = None,
visibility: Optional[str] = None, visibility: Optional[str] = None,
status: Optional[str] = None, status: Optional[str] = None,
search: Optional[str] = None, search: Optional[str] = None,
include_shared: bool = True, include_shared: bool = True,
page: int = 1, page: int = 1,
pagesize: int = 10, pagesize: int = 10,
) -> Tuple[List[App], int]: ) -> Tuple[List[App], int]:
"""列出工作空间中的应用(分页) """列出工作空间中的应用(分页)
@@ -759,8 +758,7 @@ class AppService:
) )
# 构建查询条件 # 构建查询条件
filters = [] filters = [App.is_active == True]
filters.append(App.is_active == True)
if type: if type:
filters.append(App.type == type) filters.append(App.type == type)
if visibility: if visibility:
@@ -813,9 +811,9 @@ class AppService:
return items, int(total) return items, int(total)
def get_apps_by_ids( def get_apps_by_ids(
self, self,
app_ids: List[str], app_ids: List[str],
workspace_id: uuid.UUID workspace_id: uuid.UUID
) -> List[App]: ) -> List[App]:
"""根据ID列表获取应用 """根据ID列表获取应用
@@ -846,11 +844,11 @@ class AppService:
# ==================== Agent 配置管理 ==================== # ==================== Agent 配置管理 ====================
def update_agent_config( def update_agent_config(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
data: app_schema.AgentConfigUpdate, data: app_schema.AgentConfigUpdate,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
) -> AgentConfig: ) -> AgentConfig:
"""更新 Agent 配置 """更新 Agent 配置
@@ -875,7 +873,8 @@ class AppService:
self._validate_workspace_access(app, workspace_id) 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() agent_cfg: Optional[AgentConfig] = self.db.scalars(stmt).first()
now = datetime.datetime.now() now = datetime.datetime.now()
@@ -918,10 +917,10 @@ class AppService:
return agent_cfg return agent_cfg
def get_agent_config( def get_agent_config(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
) -> AgentConfig: ) -> AgentConfig:
"""获取 Agent 配置 """获取 Agent 配置
@@ -948,7 +947,12 @@ class AppService:
# 只读操作,允许访问共享应用 # 只读操作,允许访问共享应用
self._validate_app_accessible(app, workspace_id) 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() config = self.db.scalars(stmt).first()
if config: if config:
@@ -1166,13 +1170,13 @@ class AppService:
# ==================== 应用发布管理 ==================== # ==================== 应用发布管理 ====================
def publish( def publish(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
publisher_id: uuid.UUID, publisher_id: uuid.UUID,
version_name: str, version_name: str,
workspace_id: Optional[uuid.UUID] = None, workspace_id: Optional[uuid.UUID] = None,
release_notes: Optional[str] = None release_notes: Optional[str] = None
) -> AppRelease: ) -> AppRelease:
"""发布应用(创建不可变快照) """发布应用(创建不可变快照)
@@ -1200,7 +1204,8 @@ class AppService:
default_model_config_id = None default_model_config_id = None
if app.type == AppType.AGENT: 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() agent_cfg = self.db.scalars(stmt).first()
if not agent_cfg: if not agent_cfg:
raise BusinessException("Agent 应用缺少配置,无法发布", BizCode.AGENT_CONFIG_MISSING) raise BusinessException("Agent 应用缺少配置,无法发布", BizCode.AGENT_CONFIG_MISSING)
@@ -1237,7 +1242,6 @@ class AppService:
# 4. 构建配置快照 # 4. 构建配置快照
config = { config = {
"model_parameters": model_parameters_to_dict(multi_agent_cfg.model_parameters), "model_parameters": model_parameters_to_dict(multi_agent_cfg.model_parameters),
"master_agent_id": str(multi_agent_cfg.master_agent_id), "master_agent_id": str(multi_agent_cfg.master_agent_id),
@@ -1264,6 +1268,7 @@ class AppService:
raise BusinessException("应用缺少有效配置,无法发布", BizCode.CONFIG_MISSING) raise BusinessException("应用缺少有效配置,无法发布", BizCode.CONFIG_MISSING)
config = { config = {
"id": str(workflow_cfg.id),
"nodes": workflow_cfg.nodes, "nodes": workflow_cfg.nodes,
"edges": workflow_cfg.edges, "edges": workflow_cfg.edges,
"variables": workflow_cfg.variables, "variables": workflow_cfg.variables,
@@ -1285,7 +1290,7 @@ class AppService:
id=uuid.uuid4(), id=uuid.uuid4(),
app_id=app_id, app_id=app_id,
version=version, version=version,
version_name = version_name, version_name=version_name,
release_notes=release_notes, release_notes=release_notes,
name=app.name, name=app.name,
description=app.description, description=app.description,
@@ -1319,10 +1324,10 @@ class AppService:
return release return release
def get_current_release( def get_current_release(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
) -> Optional[AppRelease]: ) -> Optional[AppRelease]:
"""获取当前发布版本 """获取当前发布版本
@@ -1349,10 +1354,10 @@ class AppService:
return self.db.get(AppRelease, app.current_release_id) return self.db.get(AppRelease, app.current_release_id)
def list_releases( def list_releases(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
) -> List[AppRelease]: ) -> List[AppRelease]:
"""列出应用的所有发布版本(倒序) """列出应用的所有发布版本(倒序)
@@ -1381,11 +1386,11 @@ class AppService:
return list(self.db.scalars(stmt).all()) return list(self.db.scalars(stmt).all())
def rollback( def rollback(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
version: int, version: int,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
) -> AppRelease: ) -> AppRelease:
"""回滚到指定版本 """回滚到指定版本
@@ -1434,12 +1439,12 @@ class AppService:
# ==================== 应用分享功能 ==================== # ==================== 应用分享功能 ====================
def share_app( def share_app(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
target_workspace_ids: List[uuid.UUID], target_workspace_ids: List[uuid.UUID],
user_id: uuid.UUID, user_id: uuid.UUID,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
) -> AppShare: ) -> AppShare:
"""分享应用到其他工作空间 """分享应用到其他工作空间
@@ -1457,7 +1462,6 @@ class AppService:
BusinessException: 当应用不在指定工作空间或目标工作空间无效时 BusinessException: 当应用不在指定工作空间或目标工作空间无效时
""" """
logger.info( logger.info(
"分享应用", "分享应用",
extra={ extra={
@@ -1536,11 +1540,11 @@ class AppService:
return shares return shares
def unshare_app( def unshare_app(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
target_workspace_id: uuid.UUID, target_workspace_id: uuid.UUID,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
) -> None: ) -> None:
"""取消应用分享 """取消应用分享
@@ -1594,10 +1598,10 @@ class AppService:
) )
def list_app_shares( def list_app_shares(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
) -> List[AppShare]: ) -> List[AppShare]:
"""列出应用的所有分享记录 """列出应用的所有分享记录
@@ -1637,14 +1641,14 @@ class AppService:
# ==================== 试运行功能 ==================== # ==================== 试运行功能 ====================
async def draft_run( async def draft_run(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
message: str, message: str,
conversation_id: Optional[str] = None, conversation_id: Optional[str] = None,
user_id: Optional[str] = None, user_id: Optional[str] = None,
variables: Optional[Dict[str, Any]] = None, variables: Optional[Dict[str, Any]] = None,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""试运行 Agent使用当前草稿配置 """试运行 Agent使用当前草稿配置
@@ -1736,14 +1740,14 @@ class AppService:
return result return result
async def draft_run_stream( async def draft_run_stream(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
message: str, message: str,
conversation_id: Optional[str] = None, conversation_id: Optional[str] = None,
user_id: Optional[str] = None, user_id: Optional[str] = None,
variables: Optional[Dict[str, Any]] = None, variables: Optional[Dict[str, Any]] = None,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
): ):
"""试运行 Agent流式返回 """试运行 Agent流式返回
@@ -1794,30 +1798,30 @@ class AppService:
# 4. 调用流式试运行服务 # 4. 调用流式试运行服务
draft_service = DraftRunService(self.db) draft_service = DraftRunService(self.db)
async for event in draft_service.run_stream( async for event in draft_service.run_stream(
agent_config=agent_cfg, agent_config=agent_cfg,
model_config=model_config, model_config=model_config,
message=message, message=message,
workspace_id=workspace_id, workspace_id=workspace_id,
conversation_id=conversation_id, conversation_id=conversation_id,
user_id=user_id, user_id=user_id,
variables=variables variables=variables
): ):
yield event yield event
# ==================== 多模型对比试运行 ==================== # ==================== 多模型对比试运行 ====================
async def draft_run_compare( async def draft_run_compare(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
message: str, message: str,
models: List[app_schema.ModelCompareItem], models: List[app_schema.ModelCompareItem],
conversation_id: Optional[str] = None, conversation_id: Optional[str] = None,
user_id: Optional[str] = None, user_id: Optional[str] = None,
variables: Optional[Dict[str, Any]] = None, variables: Optional[Dict[str, Any]] = None,
workspace_id: Optional[uuid.UUID] = None, workspace_id: Optional[uuid.UUID] = None,
parallel: bool = True, parallel: bool = True,
timeout: int = 60 timeout: int = 60
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""多模型对比试运行 """多模型对比试运行
@@ -1907,17 +1911,17 @@ class AppService:
return result return result
async def draft_run_compare_stream( async def draft_run_compare_stream(
self, self,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
message: str, message: str,
models: List[app_schema.ModelCompareItem], models: List[app_schema.ModelCompareItem],
conversation_id: Optional[str] = None, conversation_id: Optional[str] = None,
user_id: Optional[str] = None, user_id: Optional[str] = None,
variables: Optional[Dict[str, Any]] = None, variables: Optional[Dict[str, Any]] = None,
workspace_id: Optional[uuid.UUID] = None, workspace_id: Optional[uuid.UUID] = None,
parallel: bool = True, parallel: bool = True,
timeout: int = 60 timeout: int = 60
): ):
"""多模型对比试运行(流式返回) """多模型对比试运行(流式返回)
@@ -1982,15 +1986,15 @@ class AppService:
# 4. 调用 DraftRunService 的流式对比方法 # 4. 调用 DraftRunService 的流式对比方法
draft_service = DraftRunService(self.db) draft_service = DraftRunService(self.db)
async for event in draft_service.run_compare_stream( async for event in draft_service.run_compare_stream(
agent_config=agent_cfg, agent_config=agent_cfg,
models=model_configs, models=model_configs,
message=message, message=message,
workspace_id=workspace_id, workspace_id=workspace_id,
conversation_id=conversation_id, conversation_id=conversation_id,
user_id=user_id, user_id=user_id,
variables=variables, variables=variables,
parallel=parallel, parallel=parallel,
timeout=timeout timeout=timeout
): ):
yield event 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) 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) service = AppService(db)
return service.update_app(app_id=app_id, data=data, workspace_id=workspace_id) 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) 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 配置(向后兼容接口)""" """更新 Agent 配置(向后兼容接口)"""
service = AppService(db) service = AppService(db)
return service.update_agent_config(app_id=app_id, data=data, workspace_id=workspace_id) 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 配置(向后兼容接口)""" """更新 Agent 配置(向后兼容接口)"""
service = AppService(db) service = AppService(db)
return service.update_workflow_config(app_id=app_id, data=data, workspace_id=workspace_id) 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) service = AppService(db)
return service.get_agent_config(app_id=app_id, workspace_id=workspace_id) 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: def get_workflow_config(db: Session, *, app_id: uuid.UUID, workspace_id: uuid.UUID | None = None) -> WorkflowConfig:
"""获取 Agent 配置(向后兼容接口) """获取 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) 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) 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) service = AppService(db)
return service.get_current_release(app_id=app_id, workspace_id=workspace_id) 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( def list_apps(
db: Session, db: Session,
*, *,
workspace_id: uuid.UUID, workspace_id: uuid.UUID,
type: Optional[str] = None, type: Optional[str] = None,
visibility: Optional[str] = None, visibility: Optional[str] = None,
status: Optional[str] = None, status: Optional[str] = None,
search: Optional[str] = None, search: Optional[str] = None,
include_shared: bool = True, include_shared: bool = True,
page: int = 1, page: int = 1,
pagesize: int = 10, pagesize: int = 10,
) -> Tuple[List[App], int]: ) -> Tuple[List[App], int]:
"""列出应用(向后兼容接口)""" """列出应用(向后兼容接口)"""
service = AppService(db) service = AppService(db)
@@ -2100,9 +2116,9 @@ def list_apps(
def get_apps_by_ids( def get_apps_by_ids(
db: Session, db: Session,
app_ids: List[str], app_ids: List[str],
workspace_id: uuid.UUID workspace_id: uuid.UUID
) -> List[App]: ) -> List[App]:
"""根据ID列表获取应用向后兼容接口""" """根据ID列表获取应用向后兼容接口"""
service = AppService(db) service = AppService(db)
@@ -2112,14 +2128,14 @@ def get_apps_by_ids(
# ==================== 向后兼容的函数接口 ==================== # ==================== 向后兼容的函数接口 ====================
async def draft_run( async def draft_run(
db: Session, db: Session,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
message: str, message: str,
conversation_id: Optional[str] = None, conversation_id: Optional[str] = None,
user_id: Optional[str] = None, user_id: Optional[str] = None,
variables: Optional[Dict[str, Any]] = None, variables: Optional[Dict[str, Any]] = None,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""试运行 Agent向后兼容接口""" """试运行 Agent向后兼容接口"""
service = AppService(db) service = AppService(db)
@@ -2134,30 +2150,28 @@ async def draft_run(
async def draft_run_stream( async def draft_run_stream(
db: Session, db: Session,
*, *,
app_id: uuid.UUID, app_id: uuid.UUID,
message: str, message: str,
conversation_id: Optional[str] = None, conversation_id: Optional[str] = None,
user_id: Optional[str] = None, user_id: Optional[str] = None,
variables: Optional[Dict[str, Any]] = None, variables: Optional[Dict[str, Any]] = None,
workspace_id: Optional[uuid.UUID] = None workspace_id: Optional[uuid.UUID] = None
): ):
"""试运行 Agent 流式返回(向后兼容接口)""" """试运行 Agent 流式返回(向后兼容接口)"""
service = AppService(db) service = AppService(db)
async for event in service.draft_run_stream( async for event in service.draft_run_stream(
app_id=app_id, app_id=app_id,
message=message, message=message,
conversation_id=conversation_id, conversation_id=conversation_id,
user_id=user_id, user_id=user_id,
variables=variables, variables=variables,
workspace_id=workspace_id workspace_id=workspace_id
): ):
yield event yield event
# ==================== 依赖注入函数 ==================== # ==================== 依赖注入函数 ====================
def get_app_service( def get_app_service(

View File

@@ -711,45 +711,32 @@ class EmotionAnalyticsService:
end_user_id: str, end_user_id: str,
db: Session, db: Session,
) -> Optional[Dict[str, Any]]: ) -> Optional[Dict[str, Any]]:
"""从缓存获取个性化情绪建议 """ Redis 缓存获取个性化情绪建议
Args: Args:
end_user_id: 宿主ID用户组ID end_user_id: 宿主ID用户组ID
db: 数据库会话 db: 数据库会话(保留参数以保持接口兼容性)
Returns: Returns:
Dict: 缓存的建议数据,如果不存在或已过期返回 None Dict: 缓存的建议数据,如果不存在或已过期返回 None
""" """
try: try:
from app.repositories.emotion_suggestions_cache_repository import ( from app.cache.memory.emotion_memory import EmotionMemoryCache
EmotionSuggestionsCacheRepository,
)
logger.info(f"尝试从缓存获取情绪建议: user={end_user_id}") logger.info(f"尝试从 Redis 缓存获取情绪建议: user={end_user_id}")
cache_repo = EmotionSuggestionsCacheRepository(db) # 从 Redis 获取缓存
cache = cache_repo.get_by_end_user_id(end_user_id) cached_data = await EmotionMemoryCache.get_emotion_suggestions(end_user_id)
if cache is None: if cached_data is None:
logger.info(f"用户 {end_user_id} 的建议缓存不存在") logger.info(f"用户 {end_user_id} 的建议缓存不存在或已过期")
return None return None
# 检查是否过期 logger.info(f"成功从 Redis 缓存获取建议: user={end_user_id}")
if cache_repo.is_expired(cache): return cached_data
logger.info(f"用户 {end_user_id} 的建议缓存已过期")
return None
logger.info(f"成功从缓存获取建议: user={end_user_id}")
return {
"health_summary": cache.health_summary,
"suggestions": cache.suggestions,
"generated_at": cache.generated_at.isoformat(),
"cached": True
}
except Exception as e: except Exception as e:
logger.error(f"从缓存获取建议失败: {str(e)}", exc_info=True) logger.error(f" Redis 缓存获取建议失败: {str(e)}", exc_info=True)
return None return None
async def save_suggestions_cache( async def save_suggestions_cache(
@@ -759,30 +746,33 @@ class EmotionAnalyticsService:
db: Session, db: Session,
expires_hours: int = 24 expires_hours: int = 24
) -> None: ) -> None:
"""保存建议到缓存 """保存建议到 Redis 缓存
Args: Args:
end_user_id: 宿主ID用户组ID end_user_id: 宿主ID用户组ID
suggestions_data: 建议数据 suggestions_data: 建议数据
db: 数据库会话 db: 数据库会话(保留参数以保持接口兼容性)
expires_hours: 过期时间(小时) expires_hours: 过期时间(小时)默认24小时
""" """
try: try:
from app.repositories.emotion_suggestions_cache_repository import ( from app.cache.memory.emotion_memory import EmotionMemoryCache
EmotionSuggestionsCacheRepository,
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
) )
logger.info(f"保存建议到缓存: user={end_user_id}") if success:
logger.info(f"建议缓存保存成功: user={end_user_id}")
cache_repo = EmotionSuggestionsCacheRepository(db) else:
cache_repo.create_or_update( logger.warning(f"建议缓存保存失败: user={end_user_id}")
end_user_id=end_user_id,
health_summary=suggestions_data["health_summary"],
suggestions=suggestions_data["suggestions"],
expires_hours=expires_hours
)
logger.info(f"建议缓存保存成功: user={end_user_id}")
except Exception as e: except Exception as e:
logger.error(f"保存建议缓存失败: {str(e)}", exc_info=True) logger.error(f"保存建议缓存失败: {str(e)}", exc_info=True)

View File

@@ -418,48 +418,32 @@ class ImplicitMemoryService:
end_user_id: str, end_user_id: str,
db: Session db: Session
) -> Optional[dict]: ) -> Optional[dict]:
"""从缓存获取完整用户画像 """ Redis 缓存获取完整用户画像
Args: Args:
end_user_id: 终端用户ID end_user_id: 终端用户ID
db: 数据库会话 db: 数据库会话(保留参数以保持接口兼容性)
Returns: Returns:
Dict: 缓存的画像数据,如果不存在或已过期返回 None Dict: 缓存的画像数据,如果不存在或已过期返回 None
""" """
try: try:
from app.repositories.implicit_memory_cache_repository import ( from app.cache.memory.implicit_memory import ImplicitMemoryCache
ImplicitMemoryCacheRepository,
)
logger.info(f"尝试从缓存获取用户画像: user={end_user_id}") logger.info(f"尝试从 Redis 缓存获取用户画像: user={end_user_id}")
cache_repo = ImplicitMemoryCacheRepository(db) # 从 Redis 获取缓存
cache = cache_repo.get_by_end_user_id(end_user_id) cached_data = await ImplicitMemoryCache.get_user_profile(end_user_id)
if cache is None: if cached_data is None:
logger.info(f"用户 {end_user_id} 的画像缓存不存在") logger.info(f"用户 {end_user_id} 的画像缓存不存在或已过期")
return None return None
# 检查是否过期 logger.info(f"成功从 Redis 缓存获取用户画像: user={end_user_id}")
if cache_repo.is_expired(cache): return cached_data
logger.info(f"用户 {end_user_id} 的画像缓存已过期")
return None
logger.info(f"成功从缓存获取用户画像: user={end_user_id}")
return {
"end_user_id": cache.end_user_id,
"preferences": cache.preferences,
"portrait": cache.portrait,
"interest_areas": cache.interest_areas,
"habits": cache.habits,
"generated_at": cache.generated_at.isoformat(),
"cached": True
}
except Exception as e: except Exception as e:
logger.error(f"从缓存获取用户画像失败: {str(e)}", exc_info=True) logger.error(f" Redis 缓存获取用户画像失败: {str(e)}", exc_info=True)
return None return None
async def save_profile_cache( async def save_profile_cache(
@@ -469,32 +453,33 @@ class ImplicitMemoryService:
db: Session, db: Session,
expires_hours: int = 168 # 默认7天 expires_hours: int = 168 # 默认7天
) -> None: ) -> None:
"""保存用户画像到缓存 """保存用户画像到 Redis 缓存
Args: Args:
end_user_id: 终端用户ID end_user_id: 终端用户ID
profile_data: 画像数据 profile_data: 画像数据
db: 数据库会话 db: 数据库会话(保留参数以保持接口兼容性)
expires_hours: 过期时间小时默认168小时7天 expires_hours: 过期时间小时默认168小时7天
""" """
try: try:
from app.repositories.implicit_memory_cache_repository import ( from app.cache.memory.implicit_memory import ImplicitMemoryCache
ImplicitMemoryCacheRepository,
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
) )
logger.info(f"保存用户画像到缓存: user={end_user_id}") if success:
logger.info(f"用户画像缓存保存成功: user={end_user_id}")
cache_repo = ImplicitMemoryCacheRepository(db) else:
cache_repo.create_or_update( logger.warning(f"用户画像缓存保存失败: user={end_user_id}")
end_user_id=end_user_id,
preferences=profile_data["preferences"],
portrait=profile_data["portrait"],
interest_areas=profile_data["interest_areas"],
habits=profile_data["habits"],
expires_hours=expires_hours
)
logger.info(f"用户画像缓存保存成功: user={end_user_id}")
except Exception as e: except Exception as e:
logger.error(f"保存用户画像缓存失败: {str(e)}", exc_info=True) logger.error(f"保存用户画像缓存失败: {str(e)}", exc_info=True)

View File

@@ -4,7 +4,7 @@
import datetime import datetime
import logging import logging
import uuid import uuid
from typing import Any, Annotated, AsyncGenerator from typing import Any, Annotated, AsyncGenerator, Optional
from deprecated import deprecated from deprecated import deprecated
from fastapi import Depends from fastapi import Depends
@@ -14,15 +14,14 @@ from app.core.error_codes import BizCode
from app.core.exceptions import BusinessException from app.core.exceptions import BusinessException
from app.core.workflow.validator import validate_workflow_config from app.core.workflow.validator import validate_workflow_config
from app.db import get_db from app.db import get_db
from app.models.conversation_model import Message
from app.models.workflow_model import WorkflowConfig, WorkflowExecution from app.models.workflow_model import WorkflowConfig, WorkflowExecution
from app.repositories.conversation_repository import MessageRepository
from app.repositories.workflow_repository import ( from app.repositories.workflow_repository import (
WorkflowConfigRepository, WorkflowConfigRepository,
WorkflowExecutionRepository, WorkflowExecutionRepository,
WorkflowNodeExecutionRepository WorkflowNodeExecutionRepository
) )
from app.schemas import DraftRunRequest from app.schemas import DraftRunRequest
from app.services.conversation_service import ConversationService
from app.services.multi_agent_service import convert_uuids_to_str from app.services.multi_agent_service import convert_uuids_to_str
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -36,7 +35,7 @@ class WorkflowService:
self.config_repo = WorkflowConfigRepository(db) self.config_repo = WorkflowConfigRepository(db)
self.execution_repo = WorkflowExecutionRepository(db) self.execution_repo = WorkflowExecutionRepository(db)
self.node_execution_repo = WorkflowNodeExecutionRepository(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, workflow_config_id: uuid.UUID,
app_id: uuid.UUID, app_id: uuid.UUID,
trigger_type: str, trigger_type: str,
release_id: uuid.UUID | None = None,
triggered_by: uuid.UUID | None = None, triggered_by: uuid.UUID | None = None,
conversation_id: uuid.UUID | None = None, conversation_id: uuid.UUID | None = None,
input_data: dict[str, Any] | None = None input_data: dict[str, Any] | None = None
@@ -273,6 +273,7 @@ class WorkflowService:
"""创建工作流执行记录 """创建工作流执行记录
Args: Args:
release_id: 应用发布 ID
workflow_config_id: 工作流配置 ID workflow_config_id: 工作流配置 ID
app_id: 应用 ID app_id: 应用 ID
trigger_type: 触发类型 trigger_type: 触发类型
@@ -289,6 +290,7 @@ class WorkflowService:
execution = WorkflowExecution( execution = WorkflowExecution(
workflow_config_id=workflow_config_id, workflow_config_id=workflow_config_id,
app_id=app_id, app_id=app_id,
release_id=release_id,
conversation_id=conversation_id, conversation_id=conversation_id,
execution_id=execution_id, execution_id=execution_id,
trigger_type=trigger_type, trigger_type=trigger_type,
@@ -337,6 +339,7 @@ class WorkflowService:
self, self,
execution_id: str, execution_id: str,
status: str, status: str,
token_usage: int | None = None,
output_data: dict[str, Any] | None = None, output_data: dict[str, Any] | None = None,
error_message: str | None = None, error_message: str | None = None,
error_node_id: str | None = None error_node_id: str | None = None
@@ -346,6 +349,7 @@ class WorkflowService:
Args: Args:
execution_id: 执行 ID execution_id: 执行 ID
status: 状态 status: 状态
token_usage: token消耗
output_data: 输出数据 output_data: 输出数据
error_message: 错误信息 error_message: 错误信息
error_node_id: 出错节点 ID error_node_id: 出错节点 ID
@@ -364,6 +368,8 @@ class WorkflowService:
) )
execution.status = status execution.status = status
if token_usage is not None:
execution.token_usage = token_usage
if output_data is not None: if output_data is not None:
execution.output_data = convert_uuids_to_str(output_data) execution.output_data = convert_uuids_to_str(output_data)
if error_message is not None: if error_message is not None:
@@ -414,12 +420,14 @@ class WorkflowService:
payload: DraftRunRequest, payload: DraftRunRequest,
config: WorkflowConfig, config: WorkflowConfig,
workspace_id: uuid.UUID, workspace_id: uuid.UUID,
release_id: uuid.UUID | None = None,
): ):
"""运行工作流 """运行工作流
Args: Args:
workspace_id: release_id: 发布 ID
config: workspace_id:工作空间 ID
config: 配置
payload: payload:
app_id: 应用 ID app_id: 应用 ID
@@ -463,7 +471,8 @@ class WorkflowService:
trigger_type="manual", trigger_type="manual",
triggered_by=None, triggered_by=None,
conversation_id=conversation_id_uuid, conversation_id=conversation_id_uuid,
input_data=input_data input_data=input_data,
release_id=release_id,
) )
# 3. 构建工作流配置字典 # 3. 构建工作流配置字典
@@ -507,20 +516,20 @@ class WorkflowService:
# 更新执行结果 # 更新执行结果
if result.get("status") == "completed": if result.get("status") == "completed":
token_usage = result.get("token_usage", {}) or {}
self.update_execution_status( self.update_execution_status(
execution.execution_id, execution.execution_id,
"completed", "completed",
output_data=result output_data=result,
token_usage=token_usage.get("total_tokens", None)
) )
final_messages = result.get("messages", [])[init_message_length:] final_messages = result.get("messages", [])[init_message_length:]
for message in final_messages: for message in final_messages:
message_obj = Message( self.conversation_service.add_message(
conversation_id=conversation_id_uuid, conversation_id=conversation_id_uuid,
role=message["role"], 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, " logger.info(f"Workflow Run Success, "
f"execution_id: {execution.execution_id}, message count: {len(final_messages)}") f"execution_id: {execution.execution_id}, message count: {len(final_messages)}")
else: else:
@@ -562,10 +571,12 @@ class WorkflowService:
payload: DraftRunRequest, payload: DraftRunRequest,
config: WorkflowConfig, config: WorkflowConfig,
workspace_id: uuid.UUID, workspace_id: uuid.UUID,
release_id: Optional[uuid.UUID] = None,
): ):
"""运行工作流(流式) """运行工作流(流式)
Args: Args:
release_id: 发布id
workspace_id: workspace_id:
app_id: 应用 ID app_id: 应用 ID
payload: 请求对象(包含 message, variables, conversation_id 等) payload: 请求对象(包含 message, variables, conversation_id 等)
@@ -611,7 +622,8 @@ class WorkflowService:
trigger_type="manual", trigger_type="manual",
triggered_by=None, triggered_by=None,
conversation_id=conversation_id_uuid, conversation_id=conversation_id_uuid,
input_data=input_data input_data=input_data,
release_id=release_id,
) )
# 3. 构建工作流配置字典 # 3. 构建工作流配置字典
@@ -653,21 +665,21 @@ class WorkflowService:
if event.get("event") == "workflow_end": if event.get("event") == "workflow_end":
status = event.get("data", {}).get("status") status = event.get("data", {}).get("status")
token_usage = event.get("data", {}).get("token_usage", {}) or {}
if status == "completed": if status == "completed":
self.update_execution_status( self.update_execution_status(
execution.execution_id, execution.execution_id,
"completed", "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:] final_messages = event.get("data", {}).get("messages", [])[init_message_length:]
for message in final_messages: for message in final_messages:
message_obj = Message( self.conversation_service.add_message(
conversation_id=conversation_id_uuid, conversation_id=conversation_id_uuid,
role=message["role"], 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, " logger.info(f"Workflow Run Success, "
f"execution_id: {execution.execution_id}, message count: {len(final_messages)}") f"execution_id: {execution.execution_id}, message count: {len(final_messages)}")
elif status == "failed": elif status == "failed":
@@ -784,10 +796,12 @@ class WorkflowService:
# 更新执行结果 # 更新执行结果
if result.get("status") == "completed": if result.get("status") == "completed":
token_usage = result.get("data").get("token_usage", {}) or {}
self.update_execution_status( self.update_execution_status(
execution.execution_id, execution.execution_id,
"completed", "completed",
output_data=result.get("node_outputs", {}) output_data=result.get("node_outputs", {}),
token_usage=token_usage.get("total_tokens", None)
) )
else: else:
self.update_execution_status( self.update_execution_status(
@@ -882,13 +896,14 @@ class WorkflowService:
): ):
# 直接转发事件executor 已经返回正确格式) # 直接转发事件executor 已经返回正确格式)
if event.get("event") == "workflow_end": if event.get("event") == "workflow_end":
token_usage = event.get("data").get("token_usage", {}) or {}
status = event.get("data", {}).get("status") status = event.get("data", {}).get("status")
if status == "completed": if status == "completed":
self.update_execution_status( self.update_execution_status(
execution_id, execution_id,
"completed", "completed",
output_data=event.get("data") output_data=event.get("data"),
token_usage=token_usage.get("total_tokens", None)
) )
elif status == "failed": elif status == "failed":
self.update_execution_status( self.update_execution_status(

View File

@@ -53,7 +53,7 @@ nodes:
type: end type: end
name: 结束 name: 结束
config: config:
output: "{{ llm_qa.output }}" output: "{{llm_qa.output}}"
position: position:
x: 900 x: 900
y: 100 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: def workflow_config_4_app_release(release: AppRelease) -> WorkflowConfig:
config_dict = release.config 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( config = WorkflowConfig(
id=source_config_id, id=config_dict.get("id"),
app_id=release.app_id, app_id=release.app_id,
nodes=config_dict.get("nodes", []), nodes=config_dict.get("nodes", []),
edges=config_dict.get("edges", []), edges=config_dict.get("edges", []),

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

@@ -18,174 +18,180 @@ import type { TestParams } from '@/views/MemoryConversation'
import type { EndUser } from '@/views/UserMemoryDetail/types' import type { EndUser } from '@/views/UserMemoryDetail/types'
import { handleSSE, type SSEMessage } from '@/utils/stream' import { handleSSE, type SSEMessage } from '@/utils/stream'
// 记忆对话 // Memory conversation
export const readService = (query: TestParams) => { export const readService = (query: TestParams) => {
return request.post('/memory/read_service', query) return request.post('/memory/read_service', query)
} }
/****************** 记忆看板 相关接口 *******************************/ /****************** Memory Dashboard APIs *******************************/
// 记忆看板-记忆总量 // Memory Dashboard - Total memory count
export const getTotalMemoryCount = () => { export const getTotalMemoryCount = () => {
return request.get(`/dashboard/total_memory_count`) return request.get(`/dashboard/total_memory_count`)
} }
// 记忆看板-知识库类型分布 // Memory Dashboard - Knowledge base type distribution
export const getKbTypes = () => { export const getKbTypes = () => {
return request.get(`/memory/stats/types`) return request.get(`/memory/stats/types`)
} }
// 记忆看板-热门记忆标签 // Memory Dashboard - Hot memory tags
export const getHotMemoryTags = () => { export const getHotMemoryTags = () => {
return request.get(`/memory-storage/analytics/hot_memory_tags`) return request.get(`/memory-storage/analytics/hot_memory_tags`)
} }
// 记忆看板-最近活动统计 // Memory Dashboard - Recent activity statistics
export const getRecentActivityStats = () => { export const getRecentActivityStats = () => {
return request.get(`/memory-storage/analytics/recent_activity_stats`) return request.get(`/memory-storage/analytics/recent_activity_stats`)
} }
// 记忆看板-记忆增长趋势 // Memory Dashboard - Memory growth trend
export const getMemoryIncrement = (limit: number) => { export const getMemoryIncrement = (limit: number) => {
return request.get(`/dashboard/memory_increment`, { limit }) return request.get(`/dashboard/memory_increment`, { limit })
} }
// 记忆看板-API调用趋势 // Memory Dashboard - API call trend
export const getApiTrend = () => { export const getApiTrend = () => {
return request.get(`/dashboard/api_increment`) return request.get(`/dashboard/api_increment`)
} }
// 记忆看板-总数据 // Memory Dashboard - Total data
export const getDashboardData = () => { export const getDashboardData = () => {
return request.get(`/dashboard/dashboard_data`) return request.get(`/dashboard/dashboard_data`)
} }
/*************** end 记忆看板 相关接口 ******************************/ /*************** end Memory Dashboard APIs ******************************/
/****************** 用户记忆 相关接口 *******************************/ /****************** User Memory APIs *******************************/
export const userMemoryListUrl = '/dashboard/end_users' export const userMemoryListUrl = '/dashboard/end_users'
export const getUserMemoryList = () => { export const getUserMemoryList = () => {
return request.get(userMemoryListUrl) return request.get(userMemoryListUrl)
} }
// 用户记忆-用户记忆总量 // User Memory - Total end users
export const getTotalEndUsers = () => { export const getTotalEndUsers = () => {
return request.get(`/dashboard/total_end_users`) return request.get(`/dashboard/total_end_users`)
} }
// 用户记忆-用户详情 // User Memory - User profile
export const getUserProfile = (end_user_id: string) => { export const getUserProfile = (end_user_id: string) => {
return request.get(`/memory/analytics/user_profile`, { end_user_id }) return request.get(`/memory/analytics/user_profile`, { end_user_id })
} }
// 用户记忆-记忆洞察 // User Memory - Memory insight
export const getMemoryInsightReport = (end_user_id: string) => { export const getMemoryInsightReport = (end_user_id: string) => {
return request.get(`/memory-storage/analytics/memory_insight/report`, { end_user_id }) return request.get(`/memory-storage/analytics/memory_insight/report`, { end_user_id })
} }
// 用户记忆-用户摘要 // User Memory - User summary
export const getUserSummary = (end_user_id: string) => { export const getUserSummary = (end_user_id: string) => {
return request.get(`/memory-storage/analytics/user_summary`, { end_user_id }) return request.get(`/memory-storage/analytics/user_summary`, { end_user_id })
} }
// 记忆分类 // Memory classification
export const getNodeStatistics = (end_user_id: string) => { export const getNodeStatistics = (end_user_id: string) => {
return request.get(`/memory-storage/analytics/node_statistics`, { end_user_id }) return request.get(`/memory-storage/analytics/node_statistics`, { end_user_id })
} }
// 基本信息 // Basic information
export const getEndUserProfile = (end_user_id: string) => { export const getEndUserProfile = (end_user_id: string) => {
return request.get(`/memory-storage/read_end_user/profile`, { end_user_id }) return request.get(`/memory-storage/read_end_user/profile`, { end_user_id })
} }
export const updatedEndUserProfile = (values: EndUser) => { export const updatedEndUserProfile = (values: EndUser) => {
return request.post(`/memory-storage/updated_end_user/profile`, values) return request.post(`/memory-storage/updated_end_user/profile`, values)
} }
// 用户记忆-关系网络 // User Memory - Relationship network
export const getMemorySearchEdges = (end_user_id: string) => { export const getMemorySearchEdges = (end_user_id: string) => {
return request.get(`/memory-storage/analytics/graph_data`, { end_user_id }) return request.get(`/memory-storage/analytics/graph_data`, { end_user_id })
} }
// 用户记忆-用户兴趣分布 // User Memory - User interest distribution
export const getHotMemoryTagsByUser = (end_user_id: string) => { export const getHotMemoryTagsByUser = (end_user_id: string) => {
return request.get(`/memory/analytics/hot_memory_tags/by_user`, { end_user_id }) return request.get(`/memory/analytics/hot_memory_tags/by_user`, { end_user_id })
} }
// 用户记忆-记忆总量 // User Memory - Total memory count
export const getTotalMemoryCountByUser = (end_user_id: string) => { export const getTotalMemoryCountByUser = (end_user_id: string) => {
return request.get(`/memory-storage/search`, { end_user_id }) return request.get(`/memory-storage/search`, { end_user_id })
} }
// RAG 用户记忆-记忆总量 // RAG User Memory - Total memory count
export const getTotalRagMemoryCountByUser = (end_user_id: string) => { export const getTotalRagMemoryCountByUser = (end_user_id: string) => {
return request.get(`/dashboard/current_user_rag_total_num`, { end_user_id }) return request.get(`/dashboard/current_user_rag_total_num`, { end_user_id })
} }
// RAG 用户记忆-用户摘要 // RAG User Memory - User summary
export const getChunkSummaryTag = (end_user_id: string) => { export const getChunkSummaryTag = (end_user_id: string) => {
return request.get(`/dashboard/chunk_summary_tag`, { end_user_id }) return request.get(`/dashboard/chunk_summary_tag`, { end_user_id })
} }
// RAG 用户记忆-记忆洞察 // RAG User Memory - Memory insight
export const getChunkInsight = (end_user_id: string) => { export const getChunkInsight = (end_user_id: string) => {
return request.get(`/dashboard/chunk_insight`, { end_user_id }) return request.get(`/dashboard/chunk_insight`, { end_user_id })
} }
// RAG 用户记忆-存储内容 // RAG User Memory - Storage content
export const getRagContent = (end_user_id: string) => { export const getRagContent = (end_user_id: string) => {
return request.get(`/dashboard/rag_content`, { end_user_id, limit: 20 }) return request.get(`/dashboard/rag_content`, { end_user_id, limit: 20 })
} }
// 情感分布分析 // Emotion distribution analysis
export const getWordCloud = (group_id: string) => { export const getWordCloud = (group_id: string) => {
return request.post(`/memory/emotion-memory/wordcloud`, { group_id, limit: 20 }) return request.post(`/memory/emotion-memory/wordcloud`, { group_id, limit: 20 })
} }
// 高频情绪关键词 // High-frequency emotion keywords
export const getEmotionTags = (group_id: string) => { export const getEmotionTags = (group_id: string) => {
return request.post(`/memory/emotion-memory/tags`, { group_id, limit: 20 }) return request.post(`/memory/emotion-memory/tags`, { group_id, limit: 20 })
} }
// 情绪健康指数 // Emotion health index
export const getEmotionHealth = (group_id: string) => { export const getEmotionHealth = (group_id: string) => {
return request.post(`/memory/emotion-memory/health`, { group_id, limit: 20 }) return request.post(`/memory/emotion-memory/health`, { group_id, limit: 20 })
} }
// 个性化建议 // Personalized suggestions
export const getEmotionSuggestions = (group_id: string) => { export const getEmotionSuggestions = (group_id: string) => {
return request.post(`/memory/emotion-memory/suggestions`, { group_id, limit: 20 }) return request.post(`/memory/emotion-memory/suggestions`, { group_id, limit: 20 })
} }
export const generateSuggestions = (end_user_id: string) => {
return request.post(`/memory/emotion-memory/generate_suggestions`, { end_user_id })
}
export const analyticsRefresh = (end_user_id: string) => { export const analyticsRefresh = (end_user_id: string) => {
return request.post('/memory-storage/analytics/generate_cache', { end_user_id }) return request.post('/memory-storage/analytics/generate_cache', { end_user_id })
} }
// 遗忘 // Forgetting stats
export const getForgetStats = (group_id: string) => { export const getForgetStats = (group_id: string) => {
return request.get(`/memory/forget-memory/stats`, { group_id }) return request.get(`/memory/forget-memory/stats`, { group_id })
} }
// 隐性记忆-偏好 // Implicit Memory - Preferences
export const getImplicitPreferences = (end_user_id: string) => { export const getImplicitPreferences = (end_user_id: string) => {
return request.get(`/memory/implicit-memory/preferences/${end_user_id}`) return request.get(`/memory/implicit-memory/preferences/${end_user_id}`)
} }
// 隐性记忆-核心特质 // Implicit Memory - Core traits
export const getImplicitPortrait = (end_user_id: string) => { export const getImplicitPortrait = (end_user_id: string) => {
return request.get(`/memory/implicit-memory/portrait/${end_user_id}`) return request.get(`/memory/implicit-memory/portrait/${end_user_id}`)
} }
// 隐性记忆-兴趣领域分布 // Implicit Memory - Interest areas distribution
export const getImplicitInterestAreas = (end_user_id: string) => { export const getImplicitInterestAreas = (end_user_id: string) => {
return request.get(`/memory/implicit-memory/interest-areas/${end_user_id}`) return request.get(`/memory/implicit-memory/interest-areas/${end_user_id}`)
} }
// 隐性记忆-用户习惯分析 // Implicit Memory - User habits analysis
export const getImplicitHabits = (end_user_id: string) => { export const getImplicitHabits = (end_user_id: string) => {
return request.get(`/memory/implicit-memory/habits/${end_user_id}`) return request.get(`/memory/implicit-memory/habits/${end_user_id}`)
} }
// 短期记忆 export const generateProfile = (end_user_id: string) => {
return request.post(`/memory/implicit-memory/generate_profile`, { end_user_id })
}
// Short-term memory
export const getShortTerm = (end_user_id: string) => { export const getShortTerm = (end_user_id: string) => {
return request.get(`/memory/short/short_term`, { end_user_id }) return request.get(`/memory/short/short_term`, { end_user_id })
} }
// 感知记忆-视觉记忆 // Perceptual Memory - Visual memory
export const getPerceptualLastVisual = (end_user: string) => { export const getPerceptualLastVisual = (end_user: string) => {
return request.get(`/memory/perceptual/${end_user}/last_visual`) return request.get(`/memory/perceptual/${end_user}/last_visual`)
} }
// 感知记忆-音频记忆 // Perceptual Memory - Audio memory
export const getPerceptualLastListen = (end_user: string) => { export const getPerceptualLastListen = (end_user: string) => {
return request.get(`/memory/perceptual/${end_user}/last_listen`) return request.get(`/memory/perceptual/${end_user}/last_listen`)
} }
// 感知记忆-文本记忆 // Perceptual Memory - Text memory
export const getPerceptualLastText = (end_user: string) => { export const getPerceptualLastText = (end_user: string) => {
return request.get(`/memory/perceptual/${end_user}/last_text`) return request.get(`/memory/perceptual/${end_user}/last_text`)
} }
// 感知记忆-感知记忆时间线 // Perceptual Memory - Perceptual memory timeline
export const getPerceptualTimeline = (end_user: string) => { export const getPerceptualTimeline = (end_user: string) => {
return request.get(`/memory/perceptual/${end_user}/timeline`) return request.get(`/memory/perceptual/${end_user}/timeline`)
} }
// 情景记忆-总览 // Episodic Memory - Overview
export const getEpisodicOverview = (data: { end_user_id: string; time_range: string; episodic_type: string; } ) => { export const getEpisodicOverview = (data: { end_user_id: string; time_range: string; episodic_type: string; } ) => {
return request.post(`/memory/episodic-memory/overview`, data) return request.post(`/memory/episodic-memory/overview`, data)
} }
export const getEpisodicDetail = (data: { end_user_id: string; summary_id: string; } ) => { export const getEpisodicDetail = (data: { end_user_id: string; summary_id: string; } ) => {
return request.post(`/memory/episodic-memory/details`, data) return request.post(`/memory/episodic-memory/details`, data)
} }
// 关系演化 // Relationship evolution
export const getRelationshipEvolution = (data: { id: string; label: string; } ) => { export const getRelationshipEvolution = (data: { id: string; label: string; } ) => {
return request.get(`/memory-storage/memory_space/relationship_evolution`, data) return request.get(`/memory-storage/memory_space/relationship_evolution`, data)
} }
// 共同记忆时间线 // Shared memory timeline
export const getTimelineMemories = (data: { id: string; label: string; }) => { export const getTimelineMemories = (data: { id: string; label: string; }) => {
return request.get(`/memory-storage/memory_space/timeline_memories`, data) return request.get(`/memory-storage/memory_space/timeline_memories`, data)
} }
@@ -207,72 +213,72 @@ export const getConversationDetail = (end_user: string, conversation_id: string)
export const forgetTrigger = (data: { max_merge_batch_size: number; min_days_since_access: number; end_user_id: string;}) => { export const forgetTrigger = (data: { max_merge_batch_size: number; min_days_since_access: number; end_user_id: string;}) => {
return request.post(`/memory/forget-memory/trigger`, data) return request.post(`/memory/forget-memory/trigger`, data)
} }
/*************** end 用户记忆 相关接口 ******************************/ /*************** end User Memory APIs ******************************/
/****************** 记忆管理 相关接口 *******************************/ /****************** Memory Management APIs *******************************/
// 记忆管理-获取所有配置 // Memory Management - Get all configurations
export const memoryConfigListUrl = '/memory-storage/read_all_config' export const memoryConfigListUrl = '/memory-storage/read_all_config'
export const getMemoryConfigList = () => { export const getMemoryConfigList = () => {
return request.get(memoryConfigListUrl) return request.get(memoryConfigListUrl)
} }
// 记忆管理-创建配置 // Memory Management - Create configuration
export const createMemoryConfig = (values: MemoryFormData) => { export const createMemoryConfig = (values: MemoryFormData) => {
return request.post('/memory-storage/create_config', values) return request.post('/memory-storage/create_config', values)
} }
// 记忆管理-更新配置 // Memory Management - Update configuration
export const updateMemoryConfig = (values: MemoryFormData) => { export const updateMemoryConfig = (values: MemoryFormData) => {
return request.post('/memory-storage/update_config', values) return request.post('/memory-storage/update_config', values)
} }
// 记忆管理-删除配置 // Memory Management - Delete configuration
export const deleteMemoryConfig = (config_id: number) => { export const deleteMemoryConfig = (config_id: number) => {
return request.delete(`/memory-storage/delete_config?config_id=${config_id}`) return request.delete(`/memory-storage/delete_config?config_id=${config_id}`)
} }
// 遗忘引擎-获取配置 // Forgetting Engine - Get configuration
export const getMemoryForgetConfig = (config_id: number | string) => { export const getMemoryForgetConfig = (config_id: number | string) => {
return request.get('/memory/forget-memory/read_config', { config_id }) return request.get('/memory/forget-memory/read_config', { config_id })
} }
// 遗忘引擎-更新配置 // Forgetting Engine - Update configuration
export const updateMemoryForgetConfig = (values: ForgetConfigForm) => { export const updateMemoryForgetConfig = (values: ForgetConfigForm) => {
return request.post('/memory/forget-memory/update_config', values) return request.post('/memory/forget-memory/update_config', values)
} }
// 记忆萃取引擎-获取配置 // Memory Extraction Engine - Get configuration
export const getMemoryExtractionConfig = (config_id: number | string) => { export const getMemoryExtractionConfig = (config_id: number | string) => {
return request.get('/memory-storage/read_config_extracted', { config_id: config_id }) return request.get('/memory-storage/read_config_extracted', { config_id: config_id })
} }
// 记忆萃取引擎-更新配置 // Memory Extraction Engine - Update configuration
export const updateMemoryExtractionConfig = (values: ExtractionConfigForm) => { export const updateMemoryExtractionConfig = (values: ExtractionConfigForm) => {
return request.post('/memory-storage/update_config_extracted', values) return request.post('/memory-storage/update_config_extracted', values)
} }
// 记忆萃取引擎-试运行 // Memory Extraction Engine - Pilot run
export const pilotRunMemoryExtractionConfig = (values: { config_id: number | string; dialogue_text: string; }, onMessage?: (data: SSEMessage[]) => void) => { export const pilotRunMemoryExtractionConfig = (values: { config_id: number | string; dialogue_text: string; }, onMessage?: (data: SSEMessage[]) => void) => {
return handleSSE('/memory-storage/pilot_run', values, onMessage) return handleSSE('/memory-storage/pilot_run', values, onMessage)
} }
// 情绪引擎-获取配置 // Emotion Engine - Get configuration
export const getMemoryEmotionConfig = (config_id: number | string) => { export const getMemoryEmotionConfig = (config_id: number | string) => {
return request.get('/memory/emotion/read_config', { config_id: config_id }) return request.get('/memory/emotion/read_config', { config_id: config_id })
} }
// 情绪引擎-更新配置 // Emotion Engine - Update configuration
export const updateMemoryEmotionConfig = (values: EmotionConfig) => { export const updateMemoryEmotionConfig = (values: EmotionConfig) => {
return request.post('/memory/emotion/updated_config', values) return request.post('/memory/emotion/updated_config', values)
} }
// 反思引擎-获取配置 // Reflection Engine - Get configuration
export const getMemoryReflectionConfig = (config_id: number | string) => { export const getMemoryReflectionConfig = (config_id: number | string) => {
return request.get('/memory/reflection/configs', { config_id: config_id }) return request.get('/memory/reflection/configs', { config_id: config_id })
} }
// 反思引擎-更新配置 // Reflection Engine - Update configuration
export const updateMemoryReflectionConfig = (values: SelfReflectionEngineConfig) => { export const updateMemoryReflectionConfig = (values: SelfReflectionEngineConfig) => {
return request.post('/memory/reflection/save', values) return request.post('/memory/reflection/save', values)
} }
// 反思引擎-试运行 // Reflection Engine - Pilot run
export const pilotRunMemoryReflectionConfig = (values: { config_id: number | string; language_type: string; }) => { export const pilotRunMemoryReflectionConfig = (values: { config_id: number | string; language_type: string; }) => {
return request.get('/memory/reflection/run', values) return request.get('/memory/reflection/run', values)
} }
/*************** end 记忆管理 相关接口 ******************************/ /*************** end Memory Management APIs ******************************/
/****************** API参数 相关接口 *******************************/ /****************** API Parameters APIs *******************************/
export const getMemoryApi = () => { export const getMemoryApi = () => {
return request.get('/memory/docs/api') return request.get('/memory/docs/api')
} }
/*************** end API参数 相关接口 ******************************/ /*************** end API Parameters APIs ******************************/

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', key: '1',
label: (<> label: (<>
<div>{user.username}</div> <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

@@ -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 ( return (
<div className="rb:relative"> <div className="rb:relative" onKeyDown={handleKeyDown} tabIndex={0}>
<style>{` <style>{`
.html-comment { .html-comment {
color: #999; color: #999;

View File

@@ -1,3 +1,5 @@
.rb-modal .ant-modal-header { .rb-modal .ant-modal-footer .ant-btn {
margin-bottom: 24px; height: 32px !important;
padding: 0 15px !important;
font-size: 14px !important;
} }

View File

@@ -9,6 +9,7 @@
import { type FC } from 'react' import { type FC } from 'react'
import { Modal, type ModalProps } from 'antd' import { Modal, type ModalProps } from 'antd'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import './index.css'
const RbModal: FC<ModalProps> = ({ const RbModal: FC<ModalProps> = ({
onOk, onOk,
onCancel, onCancel,

View File

@@ -1,5 +1,5 @@
import { useState, type FC, useCallback, useRef } from 'react'; import { useState, type FC, useCallback, useRef } from 'react';
import { Input } from 'antd'; import { Input, type InputProps } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import searchIcon from '@/assets/images/search.svg' import searchIcon from '@/assets/images/search.svg'
@@ -11,6 +11,7 @@ interface SearchInputProps {
defaultValue?: string; defaultValue?: string;
style?: Record<string, string | number>; style?: Record<string, string | number>;
className?: string; className?: string;
size?: InputProps['size']
} }
const SearchInput: FC<SearchInputProps> = ({ const SearchInput: FC<SearchInputProps> = ({
@@ -79,7 +80,7 @@ const SearchInput: FC<SearchInputProps> = ({
return ( return (
<Input <Input
allowClear 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')} placeholder={placeholder || t('user.searchPlaceholder')}
value={value} value={value}
onChange={handleChange} onChange={handleChange}

View File

@@ -417,7 +417,8 @@ export const en = {
refresh: 'Refresh', refresh: 'Refresh',
return: 'Return', return: 'Return',
statusEnabled: 'Available', statusEnabled: 'Available',
statusDisabled: 'Unavailable' statusDisabled: 'Unavailable',
remove: 'Remove',
}, },
model: { model: {
searchPlaceholder: 'search model…', searchPlaceholder: 'search model…',
@@ -1798,9 +1799,11 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
temperature: 'Temperature', temperature: 'Temperature',
max_tokens: 'Max Tokens', max_tokens: 'Max Tokens',
context: 'Context', context: 'Context',
contextPlaceholder: '{x} Set Variable',
memory: 'Memory', memory: 'Memory',
enable_window: 'Memory Window', enable_window: 'Memory Window',
inner: 'Built-in', inner: 'Built-in',
messagesPlaceholder: 'Write prompts here, type "{" to insert variables, type "insert" to insert',
}, },
start: { start: {
variables: 'Input Fields', variables: 'Input Fields',
@@ -1811,7 +1814,6 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
array: 'Dropdown Options', array: 'Dropdown Options',
object: 'Object', object: 'Object',
addVariable: 'Add Variable',
editVariable: 'Edit Variable', editVariable: 'Edit Variable',
variableType: 'Variable Type', variableType: 'Variable Type',
variableName: 'Variable Name', variableName: 'Variable Name',
@@ -1835,6 +1837,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
'parameter-extractor': { 'parameter-extractor': {
model_id: 'Model', model_id: 'Model',
text: 'Input Variable', text: 'Input Variable',
textPlaceholder: '{x} Set Variable',
params: 'Extract Parameters', params: 'Extract Parameters',
prompt: 'Instruction', prompt: 'Instruction',
@@ -1855,6 +1858,8 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
'array[number]': 'Array[Number]', 'array[number]': 'Array[Number]',
'array[boolean]': 'Array[Boolean]', 'array[boolean]': 'Array[Boolean]',
'array[object]': 'Array[Object]', 'array[object]': 'Array[Object]',
addParams: 'Add Extract Variable',
promptPlaceholder: 'Write prompts here, type "{" to insert variables, type "insert" to insert',
}, },
'var-aggregator': { 'var-aggregator': {
group: 'Aggregation Group', group: 'Aggregation Group',
@@ -1924,6 +1929,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
loop: { loop: {
cycle_vars: 'Loop Variables', cycle_vars: 'Loop Variables',
condition: 'Loop Termination Condition', condition: 'Loop Termination Condition',
addCondition: 'Add Condition',
max_loop: 'Maximum Loop Count', max_loop: 'Maximum Loop Count',
}, },
assigner: { assigner: {
@@ -1960,6 +1966,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
type: 'Type', type: 'Type',
value: 'Value', value: 'Value',
addCase: 'Add Condition', addCase: 'Add Condition',
addVariable: 'Add Variables',
}, },
clear: 'Clear', clear: 'Clear',

View File

@@ -965,7 +965,8 @@ export const zh = {
refresh: '刷新', refresh: '刷新',
return: '返回', return: '返回',
statusEnabled: '可用', statusEnabled: '可用',
statusDisabled: '不可用' statusDisabled: '不可用',
remove: '删除',
}, },
product: { product: {
applicationManagement: '应用管理', applicationManagement: '应用管理',
@@ -1891,9 +1892,11 @@ export const zh = {
temperature: '温度', temperature: '温度',
max_tokens: '最大令牌数', max_tokens: '最大令牌数',
context: '上下文', context: '上下文',
contextPlaceholder: '{x} 设置变量',
memory: '记忆', memory: '记忆',
enable_window: '记忆窗口', enable_window: '记忆窗口',
inner: '内置', inner: '内置',
messagesPlaceholder: '在此处编写提示,输入“{”插入变量输入“insert”插入',
}, },
start: { start: {
variables: '输入字段', variables: '输入字段',
@@ -1904,7 +1907,6 @@ export const zh = {
array: '下拉选项', array: '下拉选项',
object: '对象', object: '对象',
addVariable: '添加变量',
editVariable: '编辑变量', editVariable: '编辑变量',
variableType: '变量类型', variableType: '变量类型',
variableName: '变量名称', variableName: '变量名称',
@@ -1924,10 +1926,12 @@ export const zh = {
query: '查询变量', query: '查询变量',
knowledge_retrieval: '知识库', knowledge_retrieval: '知识库',
recallConfig: '召回测试', recallConfig: '召回测试',
addKnowledge: '添加知识库'
}, },
'parameter-extractor': { 'parameter-extractor': {
model_id: '模型', model_id: '模型',
text: '输入变量', text: '输入变量',
textPlaceholder: '{x} 设置变量',
params: '提取参数', params: '提取参数',
prompt: '指令', prompt: '指令',
@@ -1948,6 +1952,8 @@ export const zh = {
'array[number]': 'Array[Number]', 'array[number]': 'Array[Number]',
'array[boolean]': 'Array[Boolean]', 'array[boolean]': 'Array[Boolean]',
'array[object]': 'Array[Object]', 'array[object]': 'Array[Object]',
addParams: '添加提取变量',
promptPlaceholder: '在此处编写提示,输入“{”插入变量输入“insert”插入',
}, },
'var-aggregator': { 'var-aggregator': {
group: '聚合分组', group: '聚合分组',
@@ -2017,6 +2023,7 @@ export const zh = {
loop: { loop: {
cycle_vars: '循环变量', cycle_vars: '循环变量',
condition: '循环终止条件', condition: '循环终止条件',
addCondition: '添加条件',
max_loop: '最大循环次数', max_loop: '最大循环次数',
}, },
assigner: { assigner: {
@@ -2053,6 +2060,7 @@ export const zh = {
type: '类型', type: '类型',
value: '值', value: '值',
addCase: '添加条件', addCase: '添加条件',
addVariable: '添加变量',
}, },
clear: '清空', clear: '清空',

View File

@@ -21,6 +21,8 @@ export const lightTheme: ThemeConfig = {
colorBorderSecondary: '#DFE4ED', colorBorderSecondary: '#DFE4ED',
// colorBgContainer: '#FBFDFF', // colorBgContainer: '#FBFDFF',
colorError: '#FF5D34', colorError: '#FF5D34',
sizeSM: 12,
fontSizeSM: 12,
}, },
components: { components: {
Layout: { Layout: {
@@ -86,6 +88,7 @@ export const lightTheme: ThemeConfig = {
rowSelectedBg: '#E9F1FF', rowSelectedBg: '#E9F1FF',
rowSelectedHoverBg: '#F0F3F8', rowSelectedHoverBg: '#F0F3F8',
cellPaddingBlock: 8, cellPaddingBlock: 8,
cellFontSizeSM: 12,
// cellPaddingInline: 24, // cellPaddingInline: 24,
selectionColumnWidth: 48, selectionColumnWidth: 48,
@@ -95,6 +98,13 @@ export const lightTheme: ThemeConfig = {
lastItemColor: '#212332', lastItemColor: '#212332',
linkColor: '#5B6167', linkColor: '#5B6167',
linkHoverColor: '#212332', linkHoverColor: '#212332',
},
Input: {
inputFontSizeSM: 12,
controlHeightSM: 26
},
Select: {
lineHeightSM: 26
} }
} }
}; };

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 { 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 type { MenuProps } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import styles from '../index.module.css' import styles from '../index.module.css'
@@ -141,10 +141,12 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
{/* <Button type="primary">{t('workflow.export')}</Button> */} {/* <Button type="primary">{t('workflow.export')}</Button> */}
<img src={logoutIcon} className="rb:w-4 rb:h-4 rb:cursor-pointer" onClick={goToApplication} /> <img src={logoutIcon} className="rb:w-4 rb:h-4 rb:cursor-pointer" onClick={goToApplication} />
</div> </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}> : <Flex justify="flex-end">
<img src={logoutIcon} className="rb:mr-2 rb:w-4 rb:h-4" /> <div className="rb:h-8 rb:flex rb:items-center rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:cursor-pointer" onClick={goToApplication}>
{t('application.returnToApplicationList')} <img src={logoutIcon} className="rb:mr-2 rb:w-4 rb:h-4" />
</div> {t('application.returnToApplicationList')}
</div>
</Flex>
} }
</Header> </Header>
<ApplicationModal <ApplicationModal

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; 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 { useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import RbCard from '@/components/RbCard/Card'; import RbCard from '@/components/RbCard/Card';
@@ -9,6 +9,7 @@ import type { ConfigForm } from './types'
import CustomSelect from '@/components/CustomSelect'; import CustomSelect from '@/components/CustomSelect';
import { getModelListUrl } from '@/api/models' import { getModelListUrl } from '@/api/models'
import Tag from '@/components/Tag' import Tag from '@/components/Tag'
import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
const configList = [ const configList = [
{ {
@@ -158,23 +159,17 @@ const EmotionEngine: React.FC = () => {
</div> </div>
) )
} }
return ( return (
<div className="rb:flex rb:items-center rb:justify-between rb:mb-6"> <SwitchFormItem
<div> title={t(`emotionEngine.${config.key}`)}
<span className="rb:text-[14px] rb:font-medium rb:leading-5">{t(`emotionEngine.${config.key}`)}</span> 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>} {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 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 className="rb:mb-6"
name={config.key} disabled={!values?.emotion_enabled && config.key !== 'emotion_enabled'}
valuePropName="checked" />
className="rb:ml-2 rb:mb-0!"
>
<Switch
disabled={!values?.emotion_enabled && config.key !== 'emotion_enabled'} />
</Form.Item>
</div>
) )
})} })}
<Row gutter={16} className="rb:mt-3"> <Row gutter={16} className="rb:mt-3">

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; 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 { useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import RbCard from '@/components/RbCard/Card'; import RbCard from '@/components/RbCard/Card';
@@ -7,6 +7,7 @@ import strategyImpactSimulator from '@/assets/images/memory/strategyImpactSimula
import LineChart from './components/LineChart' import LineChart from './components/LineChart'
import { getMemoryForgetConfig, updateMemoryForgetConfig } from '@/api/memory' import { getMemoryForgetConfig, updateMemoryForgetConfig } from '@/api/memory'
import type { ConfigForm } from './types' import type { ConfigForm } from './types'
import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
const configList = [ const configList = [
{ {
@@ -155,26 +156,12 @@ const ForgettingEngine: React.FC = () => {
{configList.map(config => { {configList.map(config => {
if (config.type === 'button') { if (config.type === 'button') {
return ( return (
<div key={config.key} className="rb:mb-2"> <SwitchFormItem
<div className="rb:flex rb:items-center rb:justify-between"> title={t(`forgettingEngine.${config.key}`)}
<div> name={config.name}
<span className="rb:text-[14px] rb:font-medium rb:leading-5">{t(`forgettingEngine.${config.key}`)}</span> desc={config.type && <span>{t(`forgettingEngine.type`)}: {config.type}</span>}
</div> className="rb:mb-2"
<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>
) )
} }
return ( return (
@@ -191,8 +178,6 @@ const ForgettingEngine: React.FC = () => {
> >
{config.type === 'decimal' {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' }} /> ? <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 : null
} }
</Form.Item> </Form.Item>

View File

@@ -11,6 +11,7 @@ import { getModelList } from '@/api/models';
import type { Model } from '@/views/ModelManagement/types' import type { Model } from '@/views/ModelManagement/types'
import { configList } from './constant' import { configList } from './constant'
import Result from './components/Result' import Result from './components/Result'
import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
const keys = [ const keys = [
// 'example', // '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> <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 => ( {vo.list.map(config => (
<div key={config.label}> <div key={config.label}>
{config.control === 'button' && {config.control === 'button' &&
<div className="rb:flex rb:items-center rb:justify-between rb:mt-6"> <SwitchFormItem
<div> title={<>-{t(`memoryExtractionEngine.${config.label}`)}</>}
<span className="rb:text-[14px] rb:font-medium rb:leading-5">-{t(`memoryExtractionEngine.${config.label}`)}</span> name={config.variableName}
<ConfigDesc config={config} className="rb:ml-2" /> desc={<ConfigDesc config={config} className="rb:ml-2" />}
</div> className="rb:mt-6"
<Form.Item />
name={config.variableName}
valuePropName="checked"
className="rb:ml-2 rb:mb-0!"
>
<Switch />
</Form.Item>
</div>
} }
{config.control === 'select' && {config.control === 'select' &&
<> <>

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; 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 { useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -11,6 +11,7 @@ import CustomSelect from '@/components/CustomSelect';
import { getModelListUrl } from '@/api/models' import { getModelListUrl } from '@/api/models'
import Tag from '@/components/Tag' import Tag from '@/components/Tag'
import { useI18n } from '@/store/locale'; import { useI18n } from '@/store/locale';
import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
const configList = [ const configList = [
// 启用反思引擎 // 启用反思引擎
@@ -219,21 +220,16 @@ const SelfReflectionEngine: React.FC = () => {
} }
return ( return (
<div className="rb:flex rb:items-center rb:justify-between rb:mb-6"> <SwitchFormItem
<div> title={t(`reflectionEngine.${config.key}`)}
<span className="rb:text-[14px] rb:font-medium rb:leading-5">{t(`reflectionEngine.${config.key}`)}</span> 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>} {(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 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 className="rb:mb-6"
name={config.key} disabled={!values?.reflection_enabled && config.key !== 'reflection_enabled'}
valuePropName="checked" />
className="rb:ml-2 rb:mb-0!"
>
<Switch
disabled={!values?.reflection_enabled && config.key !== 'reflection_enabled'} />
</Form.Item>
</div>
) )
})} })}
<Row gutter={16} className="rb:mt-3"> <Row gutter={16} className="rb:mt-3">

View File

@@ -121,7 +121,7 @@ const EmotionTags: FC = () => {
})} })}
</div> </div>
</div> </div>
: <Empty size={88} className="rb:h-full" /> : <Empty size={88} className="rb:h-full rb:mb-4" />
} }
</RbCard> </RbCard>
) )

View File

@@ -1,4 +1,4 @@
import { type FC, useEffect, useState } from 'react' import { useEffect, useState, forwardRef, useImperativeHandle } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { Skeleton, Space, Progress } from 'antd'; import { Skeleton, Space, Progress } from 'antd';
@@ -20,7 +20,7 @@ interface HabitsItem {
specific_examples: string[]; specific_examples: string[];
} }
const Habits: FC = () => { const Habits = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => {
const { t } = useTranslation() const { t } = useTranslation()
const { id } = useParams() const { id } = useParams()
const [loading, setLoading] = useState<boolean>(false) const [loading, setLoading] = useState<boolean>(false)
@@ -43,6 +43,9 @@ const Habits: FC = () => {
setLoading(false) setLoading(false)
}) })
} }
useImperativeHandle(ref, () => ({
handleRefresh: getData
}));
return ( return (
<> <>
@@ -80,5 +83,5 @@ const Habits: FC = () => {
</RbCard> </RbCard>
</> </>
) )
} })
export default Habits export default Habits

View File

@@ -1,4 +1,4 @@
import { type FC, useEffect, useState } from 'react' import { useEffect, useState, forwardRef, useImperativeHandle } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { Skeleton, Progress } from 'antd'; import { Skeleton, Progress } from 'antd';
@@ -23,7 +23,7 @@ interface InterestAreasItem {
art: Item; art: Item;
} }
const InterestAreas: FC = () => { const InterestAreas = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => {
const { t } = useTranslation() const { t } = useTranslation()
const { id } = useParams() const { id } = useParams()
const [loading, setLoading] = useState<boolean>(false) const [loading, setLoading] = useState<boolean>(false)
@@ -47,6 +47,9 @@ const InterestAreas: FC = () => {
}) })
} }
useImperativeHandle(ref, () => ({
handleRefresh: getData
}));
return ( return (
<RbCard <RbCard
title={t('implicitDetail.interestAreas')} title={t('implicitDetail.interestAreas')}
@@ -70,5 +73,5 @@ const InterestAreas: FC = () => {
} }
</RbCard> </RbCard>
) )
} })
export default InterestAreas export default InterestAreas

View File

@@ -1,4 +1,4 @@
import { type FC, useEffect, useState } from 'react' import { useEffect, useState, forwardRef, useImperativeHandle } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { Skeleton, Progress } from 'antd'; import { Skeleton, Progress } from 'antd';
@@ -25,7 +25,7 @@ interface PortraitItem {
literature: Item; literature: Item;
} }
const Portrait: FC = () => { const Portrait = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => {
const { t } = useTranslation() const { t } = useTranslation()
const { id } = useParams() const { id } = useParams()
const [loading, setLoading] = useState<boolean>(false) const [loading, setLoading] = useState<boolean>(false)
@@ -49,6 +49,9 @@ const Portrait: FC = () => {
}) })
} }
useImperativeHandle(ref, () => ({
handleRefresh: getData
}));
return ( return (
<RbCard <RbCard
title={t('implicitDetail.portrait')} title={t('implicitDetail.portrait')}
@@ -73,5 +76,5 @@ const Portrait: FC = () => {
} }
</RbCard> </RbCard>
) )
} })
export default Portrait export default Portrait

View File

@@ -1,4 +1,4 @@
import { type FC, useEffect, useState, useRef, useMemo } from 'react' import { useEffect, useState, useRef, useMemo, forwardRef, useImperativeHandle } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { Row, Col, Skeleton } from 'antd' import { Row, Col, Skeleton } from 'antd'
@@ -31,7 +31,7 @@ const generateCategoryColors = (categories: string[]) => {
return colors return colors
} }
const Preferences: FC = () => { const Preferences = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => {
const { t } = useTranslation() const { t } = useTranslation()
const { id } = useParams() const { id } = useParams()
const chartRef = useRef<HTMLDivElement>(null) const chartRef = useRef<HTMLDivElement>(null)
@@ -138,6 +138,9 @@ const Preferences: FC = () => {
return selectedWord !== null && data[selectedWord].tag_name ? <>{data[selectedWord].tag_name}{t('implicitDetail.preferencesDetail')}</> : '' return selectedWord !== null && data[selectedWord].tag_name ? <>{data[selectedWord].tag_name}{t('implicitDetail.preferencesDetail')}</> : ''
}, [selectedWord, data, t]) }, [selectedWord, data, t])
useImperativeHandle(ref, () => ({
handleRefresh: getData
}));
return ( return (
<> <>
<div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-4 rb:py-2.5 rb:font-medium rb:leading-5 rb:mb-4 rb:mt-6 rb:rounded-md">{t('forgetDetail.overviewTitle')}</div> <div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-4 rb:py-2.5 rb:font-medium rb:leading-5 rb:mb-4 rb:mt-6 rb:rounded-md">{t('forgetDetail.overviewTitle')}</div>
@@ -184,6 +187,6 @@ const Preferences: FC = () => {
</Row> </Row>
</> </>
) )
} })
export default Preferences export default Preferences

View File

@@ -1,4 +1,4 @@
import { type FC, useEffect, useState } from 'react' import { useEffect, useState, forwardRef, useImperativeHandle } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
@@ -18,7 +18,7 @@ interface Suggestions {
actionable_steps: string[]; actionable_steps: string[];
}>; }>;
} }
const Suggestions: FC = () => { const Suggestions = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => {
const { t } = useTranslation() const { t } = useTranslation()
const { id } = useParams() const { id } = useParams()
const [suggestions, setSuggestions] = useState<Suggestions | null>(null) const [suggestions, setSuggestions] = useState<Suggestions | null>(null)
@@ -37,6 +37,9 @@ const Suggestions: FC = () => {
}) })
} }
useImperativeHandle(ref, () => ({
handleRefresh: getSuggestionData
}));
return ( return (
<RbCard <RbCard
title={t('statementDetail.suggestions')} title={t('statementDetail.suggestions')}
@@ -64,6 +67,6 @@ const Suggestions: FC = () => {
} }
</RbCard> </RbCard>
) )
} })
export default Suggestions export default Suggestions

View File

@@ -1,34 +1,57 @@
import { type FC } from 'react' import { forwardRef, useImperativeHandle, useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Row, Col } from 'antd' import { Row, Col } from 'antd'
import { useParams } from 'react-router-dom'
import Preferences from '../components/Preferences' import Preferences from '../components/Preferences'
import Portrait from '../components/Portrait' import Portrait from '../components/Portrait'
import InterestAreas from '../components/InterestAreas' import InterestAreas from '../components/InterestAreas'
import Habits from '../components/Habits' import Habits from '../components/Habits'
import {
generateProfile,
} from '@/api/memory'
const ImplicitDetail: FC = () => { const ImplicitDetail = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => {
const { t } = useTranslation() const { t } = useTranslation()
const { id } = useParams()
const preferencesRef = useRef<{ handleRefresh: () => void; }>(null)
const portraitRef = useRef<{ handleRefresh: () => void; }>(null)
const interestAreasRef = useRef<{ handleRefresh: () => void; }>(null)
const habitsRef = useRef<{ handleRefresh: () => void; }>(null)
const handleRefresh = () => {
if (!id) return
generateProfile(id)
.then(() => {
preferencesRef.current?.handleRefresh()
portraitRef.current?.handleRefresh()
interestAreasRef.current?.handleRefresh()
habitsRef.current?.handleRefresh()
})
}
useImperativeHandle(ref, () => ({
handleRefresh
}));
return ( return (
<div className="rb:h-full rb:max-w-266 rb:mx-auto"> <div className="rb:h-full rb:max-w-266 rb:mx-auto">
<div className="rb:text-[#5B6167] rb:leading-5 rb:mt-3">{t('implicitDetail.title')}</div> <div className="rb:text-[#5B6167] rb:leading-5 rb:mt-3">{t('implicitDetail.title')}</div>
<Preferences /> <Preferences ref={preferencesRef} />
<div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-3 rb:py-2.5 rb:font-medium rb:leading-5 rb:mb-4 rb:mt-6 rb:rounded-md">{t('implicitDetail.portraitTitle')}</div> <div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-3 rb:py-2.5 rb:font-medium rb:leading-5 rb:mb-4 rb:mt-6 rb:rounded-md">{t('implicitDetail.portraitTitle')}</div>
<div className="rb:my-3 rb:text-[#5B6167] rb:leading-5">{t('implicitDetail.portraitSubTitle')}</div> <div className="rb:my-3 rb:text-[#5B6167] rb:leading-5">{t('implicitDetail.portraitSubTitle')}</div>
<Row gutter={[16, 16]} className="rb:mt-4"> <Row gutter={[16, 16]} className="rb:mt-4">
<Col span={12}> <Col span={12}>
<Portrait /> <Portrait ref={portraitRef} />
</Col> </Col>
<Col span={12}> <Col span={12}>
<InterestAreas /> <InterestAreas ref={interestAreasRef} />
</Col> </Col>
</Row> </Row>
<Habits /> <Habits ref={habitsRef} />
</div> </div>
) )
} })
export default ImplicitDetail export default ImplicitDetail

View File

@@ -1,13 +1,27 @@
import { type FC } from 'react' import { forwardRef, useImperativeHandle, useRef } from 'react'
import { Row, Col, Space } from 'antd'; import { Row, Col, Space } from 'antd';
import { useParams } from 'react-router-dom'
import WordCloud from '../components/WordCloud' import WordCloud from '../components/WordCloud'
import EmotionTags from '../components/EmotionTags' import EmotionTags from '../components/EmotionTags'
import Health from '../components/Health' import Health from '../components/Health'
import Suggestions from '../components/Suggestions' import Suggestions from '../components/Suggestions'
import { generateSuggestions } from '@/api/memory'
const StatementDetail: FC = () => { const StatementDetail = forwardRef((_props, ref) => {
const { id } = useParams()
const suggestionsRef = useRef<{ handleRefresh: () => void; }>(null)
const handleRefresh = () => {
if (!id) return
generateSuggestions(id)
.then(() => {
suggestionsRef.current?.handleRefresh()
})
}
useImperativeHandle(ref, () => ({
handleRefresh
}));
return ( return (
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Col span={12}> <Col span={12}>
@@ -18,10 +32,10 @@ const StatementDetail: FC = () => {
</Space> </Space>
</Col> </Col>
<Col span={12}> <Col span={12}>
<Suggestions /> <Suggestions ref={suggestionsRef} />
</Col> </Col>
</Row> </Row>
) )
} })
export default StatementDetail export default StatementDetail

View File

@@ -24,6 +24,8 @@ const Detail: FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const [name, setName] = useState<string>('') const [name, setName] = useState<string>('')
const forgetDetailRef = useRef<{ handleRefresh: () => void }>(null) const forgetDetailRef = useRef<{ handleRefresh: () => void }>(null)
const statementDetailRef = useRef<{ handleRefresh: () => void }>(null)
const implicitDetailRef = useRef<{ handleRefresh: () => void }>(null)
useEffect(() => { useEffect(() => {
if (!id) return if (!id) return
@@ -45,7 +47,17 @@ const Detail: FC = () => {
navigate(`/user-memory/detail/${id}/${key}`, { replace: true }) navigate(`/user-memory/detail/${id}/${key}`, { replace: true })
} }
const handleRefresh = () => { const handleRefresh = () => {
forgetDetailRef.current?.handleRefresh() switch(type) {
case 'FORGET_MEMORY':
forgetDetailRef.current?.handleRefresh()
break;
case 'EMOTIONAL_MEMORY':
statementDetailRef.current?.handleRefresh()
break
case 'IMPLICIT_MEMORY':
implicitDetailRef.current?.handleRefresh()
break
}
} }
if (type === 'GRAPH') { if (type === 'GRAPH') {
@@ -67,16 +79,16 @@ const Detail: FC = () => {
</div> </div>
</Dropdown> </Dropdown>
} }
extra={type === 'FORGET_MEMORY' && extra={['FORGET_MEMORY', 'EMOTIONAL_MEMORY', 'IMPLICIT_MEMORY'].includes(type as string) &&
<Button type="primary" ghost className="rb:group rb:h-6! rb:px-2!" onClick={handleRefresh}> <Button type="primary" ghost className="rb:group rb:h-6! rb:px-2!" onClick={handleRefresh}>
<img src={refreshIcon} className="rb:w-4 rb:h-4" /> <img src={refreshIcon} className="rb:w-4 rb:h-4" />
{t('common.refresh')} {t('common.refresh')}
</Button>} </Button>}
/> />
<div className="rb:h-[calc(100vh-64px)] rb:overflow-y-auto rb:py-3 rb:px-4"> <div className="rb:h-[calc(100vh-64px)] rb:overflow-y-auto rb:py-3 rb:px-4">
{type === 'EMOTIONAL_MEMORY' && <StatementDetail />} {type === 'EMOTIONAL_MEMORY' && <StatementDetail ref={statementDetailRef} />}
{type === 'FORGET_MEMORY' && <ForgetDetail ref={forgetDetailRef} />} {type === 'FORGET_MEMORY' && <ForgetDetail ref={forgetDetailRef} />}
{type === 'IMPLICIT_MEMORY' && <ImplicitDetail />} {type === 'IMPLICIT_MEMORY' && <ImplicitDetail ref={implicitDetailRef} />}
{type === 'SHORT_TERM_MEMORY' && <ShortTermDetail />} {type === 'SHORT_TERM_MEMORY' && <ShortTermDetail />}
{type === 'PERCEPTUAL_MEMORY' && <PerceptualDetail />} {type === 'PERCEPTUAL_MEMORY' && <PerceptualDetail />}
{type === 'EPISODIC_MEMORY' && <EpisodicDetail />} {type === 'EPISODIC_MEMORY' && <EpisodicDetail />}

View File

@@ -2,12 +2,13 @@ import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, InputNumber, Checkbox } from 'antd'; import { Form, Input, InputNumber, Checkbox } from 'antd';
import { useTranslation } from 'react-i18next'; 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' import RbModal from '@/components/RbModal'
interface VariableEditModalProps { interface VariableEditModalProps {
refresh: (values: StartVariableItem[]) => void; refresh: (values: Variable[]) => void;
variables: StartVariableItem[] variables: Variable[]
} }
const VariableConfigModal = forwardRef<VariableConfigModalRef, VariableEditModalProps>(({ const VariableConfigModal = forwardRef<VariableConfigModalRef, VariableEditModalProps>(({
@@ -15,9 +16,9 @@ const VariableConfigModal = forwardRef<VariableConfigModalRef, VariableEditModal
}, ref) => { }, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [form] = Form.useForm<{variables: StartVariableItem[]}>(); const [form] = Form.useForm<{variables: Variable[]}>();
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [initialValues, setInitialValues] = useState<StartVariableItem[]>([]) const [initialValues, setInitialValues] = useState<Variable[]>([])
// 封装取消方法,添加关闭弹窗逻辑 // 封装取消方法,添加关闭弹窗逻辑
const handleClose = () => { const handleClose = () => {
@@ -26,7 +27,7 @@ const VariableConfigModal = forwardRef<VariableConfigModalRef, VariableEditModal
setLoading(false) setLoading(false)
}; };
const handleOpen = (values: StartVariableItem[]) => { const handleOpen = (values: Variable[]) => {
setVisible(true); setVisible(true);
form.setFieldsValue({variables: values}) form.setFieldsValue({variables: values})
setInitialValues([...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 { LexicalComposer } from '@lexical/react/LexicalComposer';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import { ContentEditable } from '@lexical/react/LexicalContentEditable'; import { ContentEditable } from '@lexical/react/LexicalContentEditable';
@@ -25,7 +25,11 @@ interface LexicalEditorProps {
options: Suggestion[]; options: Suggestion[];
variant?: 'outlined' | 'borderless'; variant?: 'outlined' | 'borderless';
height?: number; height?: number;
fontSize?: number;
lineHeight?: number;
enableJinja2?: boolean; enableJinja2?: boolean;
size?: 'default' | 'small';
type?: 'input' | 'textarea'
} }
const theme = { const theme = {
@@ -51,8 +55,9 @@ const Editor: FC<LexicalEditorProps> =({
onChange, onChange,
options, options,
variant = 'borderless', variant = 'borderless',
height = 60,
enableJinja2 = false, enableJinja2 = false,
size = 'default',
type = 'textarea'
}) => { }) => {
const [_count, setCount] = useState(0); const [_count, setCount] = useState(0);
@@ -94,12 +99,9 @@ const Editor: FC<LexicalEditorProps> =({
display: flex; display: flex;
} }
.line-numbers { .line-numbers {
background-color: #f8f9fa;
border-right: 1px solid #e1e4e8;
color: #656d76;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 12px; font-size: 12px;
line-height: 20px; line-height: 16px;
padding: 4px 8px; padding: 4px 8px;
text-align: right; text-align: right;
user-select: none; user-select: none;
@@ -142,6 +144,21 @@ const Editor: FC<LexicalEditorProps> =({
console.error(error); 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 ( return (
<LexicalComposer initialConfig={initialConfig}> <LexicalComposer initialConfig={initialConfig}>
@@ -152,7 +169,7 @@ const Editor: FC<LexicalEditorProps> =({
<div className="editor-with-line-numbers" style={{ <div className="editor-with-line-numbers" style={{
border: variant === 'borderless' ? 'none' : '1px solid #DFE4ED', border: variant === 'borderless' ? 'none' : '1px solid #DFE4ED',
borderRadius: '6px', borderRadius: '6px',
minHeight: `${height}px`, minHeight: minheight,
}}> }}>
<div className="line-numbers"> <div className="line-numbers">
<div>1</div> <div>1</div>
@@ -160,12 +177,12 @@ const Editor: FC<LexicalEditorProps> =({
<ContentEditable <ContentEditable
className="editor-content-with-numbers" className="editor-content-with-numbers"
style={{ style={{
minHeight: `${height}px`, minHeight: minheight,
padding: '4px 11px', padding: '4px 0',
outline: 'none', outline: 'none',
resize: 'none', resize: 'none',
fontSize: '14px', fontSize: fontSize,
lineHeight: '20px', lineHeight: lineHeight,
border: 'none', border: 'none',
}} }}
/> />
@@ -173,14 +190,14 @@ const Editor: FC<LexicalEditorProps> =({
) : ( ) : (
<ContentEditable <ContentEditable
style={{ style={{
minHeight: `${height}px`, minHeight: minheight,
padding: variant === 'borderless' ? '0' : '4px 11px', padding: variant === 'borderless' ? '0' : '4px 11px',
border: variant === 'borderless' ? 'none' : '1px solid #DFE4ED', border: variant === 'borderless' ? 'none' : '1px solid #DFE4ED',
borderRadius: '6px', borderRadius: '6px',
outline: 'none', outline: 'none',
resize: 'none', resize: 'none',
fontSize: '14px', fontSize: fontSize,
lineHeight: '20px', lineHeight: lineHeight,
}} }}
/> />
) )
@@ -188,12 +205,13 @@ const Editor: FC<LexicalEditorProps> =({
placeholder={ placeholder={
<div <div
style={{ style={{
minHeight: placeHolderMinheight,
position: 'absolute', position: 'absolute',
top: variant === 'borderless' ? '0' : '6px', top: variant === 'borderless' ? '0' : '6px',
left: enableJinja2 ? '59px' : (variant === 'borderless' ? '0' : '11px'), left: enableJinja2 ? '59px' : (variant === 'borderless' ? '0' : '11px'),
color: '#5B6167', color: '#A8A9AA',
fontSize: '14px', fontSize: fontSize,
lineHeight: '20px', lineHeight: placeHolderMinheight,
pointerEvents: 'none', pointerEvents: 'none',
}} }}
> >

View File

@@ -9,12 +9,21 @@ const Jinja2HighlightPlugin = () => {
return editor.registerNodeTransform(TextNode, (textNode: TextNode) => { return editor.registerNodeTransform(TextNode, (textNode: TextNode) => {
const text = textNode.getTextContent(); const text = textNode.getTextContent();
if (containsJinja2Patterns(text)) { // Skip if node already has styling (prevent infinite recursion)
const parent = textNode.getParent(); if (textNode.getStyle()) return;
if (!parent) return;
const tokens = tokenizeJinja2(text); // Skip if no Jinja2 patterns found
const newNodes = tokens.map(token => { if (!containsJinja2Patterns(text)) return;
const parent = textNode.getParent();
if (!parent) return;
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); const newNode = $createTextNode(token.text);
switch (token.type) { switch (token.type) {
@@ -30,16 +39,16 @@ const Jinja2HighlightPlugin = () => {
newNode.setStyle('color: #008000'); newNode.setStyle('color: #008000');
break; break;
case 'brace-0': case 'brace-0':
newNode.setStyle('color: #d73a49; font-family: monospace; font-weight: bold;'); newNode.setStyle('color: #155EEF; font-family: monospace; font-weight: bold;');
break; break;
case 'brace-1': case 'brace-1':
newNode.setStyle('color: #0366d6; font-family: monospace; font-weight: bold;'); newNode.setStyle('color: #369F21; font-family: monospace; font-weight: bold;');
break; break;
case 'brace-2': case 'brace-2':
newNode.setStyle('color: #28a745; font-family: monospace; font-weight: bold;'); newNode.setStyle('color: #FF5D34; font-family: monospace; font-weight: bold;');
break; break;
case 'brace-3': case 'brace-3':
newNode.setStyle('color: #6f42c1; font-family: monospace; font-weight: bold;'); newNode.setStyle('color: #5B6167; font-family: monospace; font-weight: bold;');
break; break;
case 'expression-0': case 'expression-0':
case 'expression-1': case 'expression-1':
@@ -77,7 +86,6 @@ const Jinja2HighlightPlugin = () => {
newNodes[i - 1].insertAfter(newNodes[i]); newNodes[i - 1].insertAfter(newNodes[i]);
} }
} }
}
}); });
}, [editor]); }, [editor]);

View File

@@ -1,7 +1,6 @@
import { type FC } from 'react' import { type FC } from 'react'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Form, Input, Row, Col, Select, InputNumber, Radio } from 'antd' import { Form, Input, Select, InputNumber, Radio, Button, Space } from 'antd'
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
import VariableSelect from '../VariableSelect' import VariableSelect from '../VariableSelect'
@@ -9,6 +8,7 @@ interface AssignmentListProps {
value?: Array<{ variable_selector: string; operation: string[]; value: string;}>; value?: Array<{ variable_selector: string; operation: string[]; value: string;}>;
parentName: string; parentName: string;
options: Suggestion[]; options: Suggestion[];
size?: 'small' | 'middle'
} }
const operationsObj = { const operationsObj = {
@@ -31,6 +31,7 @@ const operationsObj = {
const AssignmentList: FC<AssignmentListProps> = ({ const AssignmentList: FC<AssignmentListProps> = ({
parentName, parentName,
options = [], options = [],
size = 'small'
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const form = Form.useFormInstance(); const form = Form.useFormInstance();
@@ -39,109 +40,126 @@ const AssignmentList: FC<AssignmentListProps> = ({
<Form.List name={parentName}> <Form.List name={parentName}>
{(fields, { add, remove }) => ( {(fields, { add, remove }) => (
<> <>
<div className="rb:flex rb:justify-between"> <div className="rb:flex rb:items-center rb:justify-between rb:mb-2.5">
{t(`workflow.config.assigner.${parentName}`)} <div className="rb:text-[12px] rb:leading-4.5 rb:font-medium">
<PlusOutlined onClick={() => add({ operation: 'cover'})} /> {t(`workflow.config.assigner.${parentName}`)}
</div>
<Button
onClick={() => add({ operation: 'cover' })}
className="rb:py-0! rb:px-1! rb:text-[12px]!"
size="small"
>
+ {t('workflow.config.addVariable')}
</Button>
</div> </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 ( <Space size={10} direction="vertical" className="rb:w-full!">
<div key={key} className="rb:mb-4"> {fields.map(({ key, name, ...restField }) => {
<Row gutter={12} className="rb:mb-2!"> const variableSelector = form.getFieldValue([parentName, name, 'variable_selector']);
<Col span={14}> const selectedOption = options.find(option => `{{${option.value}}}` === variableSelector);
<Form.Item const dataType = selectedOption?.dataType;
{...restField} const operationOptions = dataType === 'number' ? operationsObj.number : operationsObj.default;
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>
<Form.Item shouldUpdate noStyle> return (
{(form) => { <div key={key} className="rb:flex rb:items-start">
const operation = form.getFieldValue([parentName, name, 'operation']); <div className="rb:flex-1">
if (operation === 'clear') return null; <div className="rb:flex rb:gap-1 rb:mb-1">
return (
<Form.Item <Form.Item
{...restField} {...restField}
name={[name, 'value']} name={[name, 'variable_selector']}
noStyle noStyle
> >
{dataType === 'number' && operation === 'cover' <VariableSelect
? <VariableSelect placeholder={t('common.pleaseSelect')}
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')))}
options={dataType ? options.filter(vo => vo.dataType === dataType) : options} popupMatchSelectWidth={false}
popupMatchSelectWidth={false} onChange={() => {
/> form.setFieldValue([parentName, name, 'operation'], undefined);
: dataType === 'number' form.setFieldValue([parentName, name, 'value'], undefined);
? <InputNumber }}
placeholder={t('common.pleaseEnter')} size={size}
className="rb:w-full!" className="rb:w-39!"
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}
/>
}
</Form.Item> </Form.Item>
); <Form.Item
}} {...restField}
</Form.Item> name={[name, 'operation']}
</div> 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> </Form.List>

View File

@@ -1,8 +1,7 @@
import { type FC } from 'react' import { type FC } from 'react'
import clsx from 'clsx' import clsx from 'clsx'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Form, Button, Select, Space, Row, Col, Divider, InputNumber, Radio, type SelectProps } from 'antd' import { Form, Button, Select, Space, Divider, InputNumber, Radio, type SelectProps } from 'antd'
import { DeleteOutlined } from '@ant-design/icons';
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
import VariableSelect from '../VariableSelect' import VariableSelect from '../VariableSelect'
@@ -247,37 +246,40 @@ const CaseList: FC<CaseListProps> = ({
{(conditionFields, { add: addCondition, remove: removeCondition }) => { {(conditionFields, { add: addCondition, remove: removeCondition }) => {
const logicalOperator = form.getFieldValue(name)?.[caseIndex]?.logical_operator || 'and' const logicalOperator = form.getFieldValue(name)?.[caseIndex]?.logical_operator || 'and'
return ( 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={clsx("rb:relative")}>
<div className="rb:flex rb:items-center rb:justify-between rb:mb-3"> <div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
<span className="rb:font-medium"> <div className="rb:text-[12px] rb:leading-4.5">
{caseIndex === 0 ? 'IF' : 'ELIF'}<br/> <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>} {caseFields.length > 1 && <span className="rb:text-[10px] rb:text-[#5B6167]"> ({`CASE ${caseIndex + 1}`})</span>}
</span> </div>
<Space> <Space>
<Button <Button
type="dashed"
onClick={() => addCondition({})} onClick={() => addCondition({})}
className="rb:py-0! rb:px-1! rb:text-[12px]!"
size="small" size="small"
> >
+ {t('workflow.config.addCase')} + {t('workflow.config.addCase')}
</Button> </Button>
{caseFields.length > 1 && <DeleteOutlined {caseFields.length > 1 &&
className="rb:text-[12px]" <Button
onClick={() => handleRemoveCase(removeCase, caseField.name, caseIndex)} className="rb:py-0! rb:px-1! rb:text-[12px]!"
/>} onClick={() => handleRemoveCase(removeCase, caseField.name, caseIndex)}
>
{t('common.remove')}
</Button>
}
</Space> </Space>
</div> </div>
{conditionFields?.length > 1 && {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: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-[calc(50%-13px)]">
<div className="rb:absolute rb:z-10 rb:left-0 rb:top-[50%] rb:transform-[translateY(-50%)]]">
<Form.Item name={[caseField.name, 'logical_operator']} noStyle > <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> </Form.Item>
</div> </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) => { {conditionFields.map((conditionField, conditionIndex) => {
const cases = form.getFieldValue(name) || []; const cases = form.getFieldValue(name) || [];
const currentCase = cases[caseIndex] || {}; const currentCase = cases[caseIndex] || {};
@@ -290,91 +292,86 @@ const CaseList: FC<CaseListProps> = ({
const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || []; const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || [];
const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined; const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined;
return ( return (
<div key={conditionField.key} className={clsx({ <div key={conditionField.key} className="rb:flex rb:items-start rb:ml-9.5 rb:mb-4">
"rb:mb-3": conditionIndex !== conditionFields.length - 1 <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", {
<div className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white"> 'rb:border-b rb:border-b-[#DFE4ED]': !hideRightField
<Row gutter={12} className="rb:mb-1"> })}>
<Col span={14}> <Form.Item name={[conditionField.name, 'left']} noStyle>
<Form.Item name={[conditionField.name, 'left']} noStyle> <VariableSelect
<VariableSelect placeholder={t('common.pleaseSelect')}
placeholder={t('common.pleaseSelect')} options={options}
options={options} size="small"
size="small" allowClear={false}
allowClear={false} popupMatchSelectWidth={false}
popupMatchSelectWidth={false} onChange={(val) => handleLeftFieldChange(caseIndex, conditionIndex, val)}
onChange={(val) => handleLeftFieldChange(caseIndex, conditionIndex, val)} className="rb:bg-white! rb:w-29.5!"
/>
</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)}
/> />
</Col> </Form.Item>
</Row> <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' {leftFieldType === 'number'
? <Row> ? <div className="rb:flex rb:items-center">
<Col span={12}> <Form.Item name={[conditionField.name, 'input_type']} noStyle>
<Form.Item name={[conditionField.name, 'input_type']} noStyle> <Select
<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')} placeholder={t('common.pleaseSelect')}
options={[{ value: 'Variable', label: 'Variable' }, { value: 'Constant', label: 'Constant' }]} options={options.filter(vo => vo.dataType === 'number')}
allowClear={false}
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
variant="borderless" variant="borderless"
onChange={() => handleInputTypeChange(caseIndex, conditionIndex)} size="small"
/> />
</Form.Item> : <InputNumber
</Col> placeholder={t('common.pleaseEnter')}
<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}
variant="borderless" variant="borderless"
className="rb:w-full!"
onChange={(value) => form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'right'], value)}
/> />
: <InputNumber }
placeholder={t('common.pleaseEnter')} </Form.Item>
variant="borderless" </div>
className="rb:w-full!"
onChange={(value) => form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'right'], value)}
/>
}
</Form.Item>
</Col>
</Row>
: <Form.Item name={[conditionField.name, 'right']} noStyle> : <Form.Item name={[conditionField.name, 'right']} noStyle>
{leftFieldType === 'boolean' {leftFieldType === 'boolean'
? <Radio.Group block> ? <Radio.Group block>
<Radio.Button value={true}>True</Radio.Button> <Radio.Button value={true}>True</Radio.Button>
<Radio.Button value={false}>False</Radio.Button> <Radio.Button value={false}>False</Radio.Button>
</Radio.Group> </Radio.Group>
: <Editor options={options} /> : <Editor options={options} size="small" type="input" />
} }
</Form.Item> </Form.Item>
} }
</>} </div>}
</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> </div>
) )
})} })}
@@ -388,6 +385,8 @@ const CaseList: FC<CaseListProps> = ({
<Button <Button
type="dashed" type="dashed"
block block
size="middle"
className="rb:text-[12px]!"
onClick={() => handleAddCase(addCase)} onClick={() => handleAddCase(addCase)}
> >
+ ELIF + ELIF
@@ -395,9 +394,9 @@ const CaseList: FC<CaseListProps> = ({
</> </>
)} )}
</Form.List> </Form.List>
<Divider />
<div className="rb:font-medium">ELSE</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] ">{t('workflow.config.if-else.else_desc')}</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 { type FC } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button, Form, Space } from 'antd'; import { Button, Form, Space } from 'antd';
import { DeleteOutlined } from '@ant-design/icons';
import { Graph, Node } from '@antv/x6'; import { Graph, Node } from '@antv/x6';
import Editor from '../../Editor'; import Editor from '../../Editor';
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
@@ -151,17 +151,15 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
const contentLength = (currentItem.class_name || '').length; const contentLength = (currentItem.class_name || '').length;
return ( 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 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"> <div className="rb:flex rb:items-center rb:gap-1">
<span className="rb:text-xs rb:text-gray-500">{contentLength}</span> <span className="rb:text-xs rb:text-gray-500">{contentLength}</span>
<Button <div
type="text" 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')]"
size="small"
icon={<DeleteOutlined />}
onClick={() => handleRemoveCategory(remove, name, index)} onClick={() => handleRemoveCategory(remove, name, index)}
/> ></div>
</div> </div>
</div> </div>
<Form.Item <Form.Item
@@ -172,6 +170,7 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
<Editor <Editor
placeholder={t('common.pleaseEnter')} placeholder={t('common.pleaseEnter')}
options={options} options={options}
size="small"
/> />
</Form.Item> </Form.Item>
</div> </div>
@@ -179,8 +178,10 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
<Button <Button
type="dashed" type="dashed"
size="middle"
block
onClick={() => handleAddCategory(add)} onClick={() => handleAddCategory(add)}
className="rb:w-full" className="rb:text-[12px]!"
> >
+ {t('workflow.config.question-classifier.addClassName')} + {t('workflow.config.question-classifier.addClassName')}
</Button> </Button>

View File

@@ -1,11 +1,10 @@
import { type FC } from 'react' import { type FC } from 'react'
import clsx from 'clsx'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Form, Button, Select, Row, Col, InputNumber, Radio, Input, type SelectProps } from 'antd' import { Form, Button, Select, InputNumber, Radio, Input, Divider, type SelectProps } from 'antd'
import { DeleteOutlined } from '@ant-design/icons';
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
import VariableSelect from '../VariableSelect' import VariableSelect from '../VariableSelect'
import Editor from '../../Editor'
interface Case { interface Case {
logical_operator: 'and' | 'or'; logical_operator: 'and' | 'or';
@@ -84,35 +83,49 @@ const ConditionList: FC<CaseListProps> = ({
return ( return (
<> <>
<Form.List name={[parentName, 'expressions']}> <Form.List name={[parentName, 'expressions']}>
{(fields, { add, remove }) => ( {(fields, { add, remove }) => {
<div> const logicalOperator = form.getFieldValue([parentName, 'logical_operator']);
return (
<div className="rb:relative"> <div className="rb:relative">
{fields.map((field, index) => { <div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
const expressions = form.getFieldValue([parentName, 'expressions']) || []; <div className="rb:text-[12px] rb:font-medium rb:leading-4.5">
const currentExpression = expressions[index] || {}; {t('workflow.config.loop.condition')}
const currentOperator = currentExpression.operator; </div>
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 ( <Button
<div key={field.key} className="rb:mb-3"> onClick={() => add({})}
{index > 0 && (<> className="rb:py-0! rb:px-1! rb:text-[12px]!"
<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> size="small"
<div className="rb:absolute rb:z-10 rb:left-0 rb:top-[50%] rb:transform-[translateY(-50%)]]"> >
<Form.Item name={[parentName, 'logical_operator']} noStyle > + {t('workflow.config.loop.addCondition')}
<Button size="small" className="rb:cursor-pointer" onClick={handleChangeLogicalOperator}>{logicalOperator}</Button> </Button>
</Form.Item> </div>
</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;
<div className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:p-3 rb:bg-white rb:ml-6"> return (
<Row gutter={8} align="middle"> <div key={field.key} className="rb:flex rb:items-start rb:ml-9.5 rb:mb-4">
<Col span={14}> <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> <Form.Item name={[field.name, 'left']} noStyle>
<VariableSelect <VariableSelect
options={options.filter(vo => options={options.filter(vo =>
@@ -124,12 +137,10 @@ const ConditionList: FC<CaseListProps> = ({
size="small" size="small"
allowClear={false} allowClear={false}
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
placeholder={t('common.pleaseSelect')}
onChange={(val) => handleLeftFieldChange(index, val)} onChange={(val) => handleLeftFieldChange(index, val)}
/> />
</Form.Item> </Form.Item>
</Col>
<Col span={8}>
<Form.Item name={[field.name, 'operator']} noStyle> <Form.Item name={[field.name, 'operator']} noStyle>
<Select <Select
options={operatorList.map(vo => ({ options={operatorList.map(vo => ({
@@ -138,84 +149,67 @@ const ConditionList: FC<CaseListProps> = ({
}))} }))}
size="small" size="small"
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
placeholder={t('common.pleaseSelect')}
/> />
</Form.Item> </Form.Item>
</Col> </div>
<Col span={2}>
<DeleteOutlined
className="rb:text-gray-400 rb:cursor-pointer rb:hover:text-red-500"
onClick={() => remove(field.name)}
/>
</Col>
{!hideRightField && <> {!hideRightField && <div className="rb:p-1">
{leftFieldType === 'number' {leftFieldType === 'number'
? <Col span={24}><Row> ? <div className="rb:flex rb:items-center">
<Col span={12}> <Form.Item name={[field.name, 'input_type']} noStyle>
<Form.Item name={[field.name, 'input_type']} noStyle> <Select
<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')} placeholder={t('common.pleaseSelect')}
options={[{ value: 'Variable', label: 'Variable' }, { value: 'Constant', label: 'Constant' }]} options={options.filter(vo => vo.dataType === 'number')}
allowClear={false}
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
variant="borderless" variant="borderless"
className="rb:w-full!" className="rb:w-full!"
onChange={() => handleInputTypeChange(index)}
/> />
</Form.Item> : <InputNumber
</Col> placeholder={t('common.pleaseEnter')}
<Col span={12}> variant="borderless"
<Form.Item name={[field.name, 'right']} noStyle> className="rb:w-full!"
{inputType === 'Variable' onChange={(value) => form.setFieldValue([parentName, 'expressions', index, 'right'], value)}
? />
<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')} />
} }
</Form.Item> </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>
} }
</>} </div>}
</div>
</Row> <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> )
) })}
})}
</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> </Form.List>
</> </>
) )

View File

@@ -1,7 +1,6 @@
import { type FC } from 'react' import { type FC } from 'react'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Form, Select, Row, Col, Input } from 'antd' import { Form, Select, Input, Button } from 'antd'
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
import VariableSelect from '../VariableSelect' import VariableSelect from '../VariableSelect'
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
@@ -20,6 +19,7 @@ interface CycleVarsListProps {
parentName: string; parentName: string;
selectedNode?: any; selectedNode?: any;
graphRef?: any; graphRef?: any;
size?: 'small' | 'middle'
} }
const types = [ const types = [
@@ -37,7 +37,8 @@ const CycleVarsList: FC<CycleVarsListProps> = ({
options, options,
parentName, parentName,
selectedNode, selectedNode,
graphRef graphRef,
size = 'middle'
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const form = Form.useFormInstance(); const form = Form.useFormInstance();
@@ -78,62 +79,56 @@ const CycleVarsList: FC<CycleVarsListProps> = ({
const availableOptions = getChildNodeVariables(); const availableOptions = getChildNodeVariables();
return ( 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-[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;
<Form.List name={parentName}> return (
{(fields, { add, remove }) => ( <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:items-center rb:justify-between rb:mb-3"> <div className="rb:flex rb:gap-1 rb:p-1 rb:border-b rb:border-b-[#DFE4ED]">
<span className="rb:text-sm rb:font-medium"></span> <Form.Item name={[name, 'name']} noStyle>
<PlusOutlined className="rb:text-gray-400 rb:cursor-pointer rb:hover:text-blue-500" onClick={() => add({ name: '', type: 'string', input_type: 'constant', value: '' })} /> <Input size={size} className="rb:w-23!" placeholder={t('common.pleaseEnter')} />
</div> </Form.Item>
{fields.map(({ key, name, ...field }, index) => { <Form.Item name={[name, 'type']} noStyle>
const currentInputType = value?.[index]?.input_type; <Select
options={types.map(key => ({
return ( value: key,
<div key={key} className="rb:mb-3 rb:border rb:border-[#DFE4ED] rb:rounded-md rb:p-3 rb:bg-white"> label: t(`workflow.config.parameter-extractor.${key}`),
<Row gutter={8} align="middle" className="rb:mb-2"> }))}
<Col span={8}> size={size}
<Form.Item name={[name, 'name']} noStyle> popupMatchSelectWidth={false}
<Input size="small" /> className="rb:w-18.5!"
</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)}
/> />
</Col> </Form.Item>
</Row> <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> <Form.Item name={[name, 'value']} noStyle>
{currentInputType === 'variable' ? ( {currentInputType === 'variable' ? (
@@ -145,22 +140,29 @@ const CycleVarsList: FC<CycleVarsListProps> = ({
return option.dataType === currentType return option.dataType === currentType
})} })}
variant="borderless"
size="small"
/> />
) : ( ) : (
<Input.TextArea <Input.TextArea
placeholder={t('common.pleaseEnter')} placeholder={t('common.pleaseEnter')}
rows={3} rows={3}
className="rb:w-full" className="rb:w-full"
variant="borderless"
/> />
)} )}
</Form.Item> </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={() => remove(name)}
)} ></div>
</Form.List> </div>
</div> )
})}
</>
)}
</Form.List>
) )
} }

View File

@@ -1,7 +1,7 @@
import { type FC } from 'react' import { type FC } from 'react'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Form, Input, Button, Row, Col } from 'antd' import { Form, Input, Button, Row, Col } from 'antd'
import { MinusCircleOutlined } from '@ant-design/icons';
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
import VariableSelect from '../VariableSelect' import VariableSelect from '../VariableSelect'
@@ -9,13 +9,15 @@ interface GroupVariableListProps {
value?: Array<{ key: string; value: string[]; }>; value?: Array<{ key: string; value: string[]; }>;
name: string; name: string;
options: Suggestion[]; options: Suggestion[];
isCanAdd: boolean isCanAdd: boolean;
size: 'small' | 'middle'
} }
const GroupVariableList: FC<GroupVariableListProps> = ({ const GroupVariableList: FC<GroupVariableListProps> = ({
name, name,
options = [], options = [],
isCanAdd = false isCanAdd = false,
size = "middle"
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const form = Form.useFormInstance(); const form = Form.useFormInstance();
@@ -54,6 +56,7 @@ const GroupVariableList: FC<GroupVariableListProps> = ({
placeholder={t('common.pleaseSelect')} placeholder={t('common.pleaseSelect')}
options={filteredOptions} options={filteredOptions}
mode="multiple" mode="multiple"
size={size}
/> />
</Form.Item> </Form.Item>
</div> </div>
@@ -76,11 +79,15 @@ const GroupVariableList: FC<GroupVariableListProps> = ({
]} ]}
noStyle 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> </Form.Item>
</Col> </Col>
{isCanAdd && <Col span={12} className="rb:flex! rb:items-center rb:justify-end"> {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>} </Col>}
</Row> </Row>
@@ -104,16 +111,22 @@ const GroupVariableList: FC<GroupVariableListProps> = ({
})() })()
} }
mode="multiple" mode="multiple"
size={size}
/> />
</Form.Item> </Form.Item>
</div> </div>
) )
})} })}
{isCanAdd && <Form.Item noStyle>
<Button type="dashed" onClick={() => add({ key: `Group${fields.length + 1}` })} block> {isCanAdd && <Button
+ {t('workflow.config.var-aggregator.addGroup')} type="dashed"
</Button> block
</Form.Item>} size="middle"
className="rb:text-[12px]!"
onClick={() => add({ key: `Group${fields.length + 1}` })}
>
+ {t('workflow.config.var-aggregator.addGroup')}
</Button>}
</> </>
)} )}
</Form.List> </Form.List>

View File

@@ -93,6 +93,7 @@ const AuthConfigModal = forwardRef<AuthConfigModalRef, AuthConfigModalProps>(({
initialValues={{ initialValues={{
auth: 'none' auth: 'none'
}} }}
size="middle"
> >
<FormItem <FormItem
name="auth" name="auth"
@@ -102,6 +103,7 @@ const AuthConfigModal = forwardRef<AuthConfigModalRef, AuthConfigModalProps>(({
]} ]}
> >
<Select <Select
size="middle"
options={[ options={[
{ value: 'none', label: t('workflow.config.http-request.none') }, { value: 'none', label: t('workflow.config.http-request.none') },
{ value: 'api_key', label: t('workflow.config.http-request.apiKey') }, { value: 'api_key', label: t('workflow.config.http-request.apiKey') },
@@ -117,6 +119,7 @@ const AuthConfigModal = forwardRef<AuthConfigModalRef, AuthConfigModalProps>(({
]} ]}
> >
<Select <Select
size="middle"
options={[ options={[
{ value: 'basic', label: t('workflow.config.http-request.basic') }, { value: 'basic', label: t('workflow.config.http-request.basic') },
{ value: 'bearer', label: t('workflow.config.http-request.bearer') }, { value: 'bearer', label: t('workflow.config.http-request.bearer') },
@@ -132,7 +135,7 @@ const AuthConfigModal = forwardRef<AuthConfigModalRef, AuthConfigModalProps>(({
{ required: true, message: t('common.pleaseEnter') } { required: true, message: t('common.pleaseEnter') }
]} ]}
> >
<Input placeholder={t('common.pleaseEnter')} /> <Input size="middle" placeholder={t('common.pleaseEnter')} />
</FormItem> </FormItem>
} }
<FormItem <FormItem
@@ -142,7 +145,7 @@ const AuthConfigModal = forwardRef<AuthConfigModalRef, AuthConfigModalProps>(({
{ required: true, message: t('common.pleaseEnter') } { required: true, message: t('common.pleaseEnter') }
]} ]}
> >
<Input placeholder={t('common.pleaseEnter')} /> <Input size="middle" placeholder={t('common.pleaseEnter')} />
</FormItem> </FormItem>
</>} </>}
</Form> </Form>

View File

@@ -1,7 +1,6 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button, Select, Table, Form, type TableProps } from 'antd'; 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 type { Suggestion } from '../../Editor/plugin/AutocompletePlugin';
import Empty from '@/components/Empty'; import Empty from '@/components/Empty';
import VariableSelect from '../VariableSelect'; import VariableSelect from '../VariableSelect';
@@ -19,6 +18,7 @@ interface EditableTableProps {
options?: Suggestion[]; options?: Suggestion[];
typeOptions?: { value: string, label: string }[] typeOptions?: { value: string, label: string }[]
filterBooleanType?: boolean; filterBooleanType?: boolean;
size?: "small"
} }
const EditableTable: React.FC<EditableTableProps> = ({ const EditableTable: React.FC<EditableTableProps> = ({
@@ -26,7 +26,8 @@ const EditableTable: React.FC<EditableTableProps> = ({
title, title,
options = [], options = [],
typeOptions = [], typeOptions = [],
filterBooleanType = false filterBooleanType = false,
size = 'small'
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -38,21 +39,24 @@ const EditableTable: React.FC<EditableTableProps> = ({
const getColumns = (remove: (index: number) => void): TableProps<TableRow>['columns'] => { const getColumns = (remove: (index: number) => void): TableProps<TableRow>['columns'] => {
const hasType = typeOptions.length > 0; const hasType = typeOptions.length > 0;
const baseWidth = hasType ? '35%' : '45%'; const cellClassName="rb:p-1!"
const contentClassName ="rb:w-[108px]! rb:text-[12px]!"
return [ return [
{ {
title: t('workflow.config.name'), title: t('workflow.config.name'),
dataIndex: 'name', dataIndex: 'name',
width: baseWidth, className: cellClassName,
render: (_: any, __: TableRow, index: number) => ( render: (_: any, __: TableRow, index: number) => (
<Form.Item name={[index, 'name']} noStyle> <Form.Item name={[index, 'name']} noStyle>
<VariableSelect <VariableSelect
placeholder={t('common.pleaseSelect')} placeholder={t('common.pleaseSelect')}
size="small" // size="small"
options={options} options={options}
filterBooleanType={filterBooleanType} filterBooleanType={filterBooleanType}
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
className={contentClassName}
size={size}
/> />
</Form.Item> </Form.Item>
) )
@@ -61,18 +65,20 @@ const EditableTable: React.FC<EditableTableProps> = ({
title: t('workflow.config.type'), title: t('workflow.config.type'),
dataIndex: 'type', dataIndex: 'type',
width: '20%', width: '20%',
className: cellClassName,
render: (_: any, __: TableRow, index: number) => ( render: (_: any, __: TableRow, index: number) => (
<Form.Item shouldUpdate noStyle> <Form.Item shouldUpdate noStyle>
{(form) => ( {(form) => (
<Form.Item name={[index, 'type']} noStyle> <Form.Item name={[index, 'type']} noStyle>
<Select <Select
placeholder={t('common.pleaseSelect')} placeholder={t('common.pleaseSelect')}
size="small" // size="small"
options={typeOptions} options={typeOptions}
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
onChange={() => { onChange={() => {
form.setFieldValue([...Array.isArray(parentName) ? parentName : [parentName], index, 'value'], undefined); form.setFieldValue([...Array.isArray(parentName) ? parentName : [parentName], index, 'value'], undefined);
}} }}
size={size}
/> />
</Form.Item> </Form.Item>
)} )}
@@ -82,7 +88,7 @@ const EditableTable: React.FC<EditableTableProps> = ({
{ {
title: t('workflow.config.value'), title: t('workflow.config.value'),
dataIndex: 'value', dataIndex: 'value',
width: baseWidth, className: cellClassName,
render: (_: any, __: TableRow, index: number) => ( render: (_: any, __: TableRow, index: number) => (
<Form.Item <Form.Item
shouldUpdate={(prevValues, currentValues) => { shouldUpdate={(prevValues, currentValues) => {
@@ -102,10 +108,12 @@ const EditableTable: React.FC<EditableTableProps> = ({
<Form.Item name={[index, 'value']} noStyle> <Form.Item name={[index, 'value']} noStyle>
<VariableSelect <VariableSelect
placeholder={t('common.pleaseSelect')} placeholder={t('common.pleaseSelect')}
size="small" // size="small"
options={filteredOptions} options={filteredOptions}
filterBooleanType={filterBooleanType} filterBooleanType={filterBooleanType}
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
className={contentClassName}
size={size}
/> />
</Form.Item> </Form.Item>
); );
@@ -116,9 +124,12 @@ const EditableTable: React.FC<EditableTableProps> = ({
{ {
title: '', title: '',
dataIndex: 'actions', dataIndex: 'actions',
width: '10%', className: cellClassName,
render: (_: any, __: TableRow, index: number) => ( 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>
) )
} }
]; ];
@@ -130,12 +141,10 @@ const EditableTable: React.FC<EditableTableProps> = ({
{(fields, { add, remove }) => { {(fields, { add, remove }) => {
const AddButton = ({ block = false }: { block?: boolean }) => ( const AddButton = ({ block = false }: { block?: boolean }) => (
<Button <Button
type={block ? "dashed" : "text"}
icon={block ? undefined : <PlusOutlined />} icon={block ? undefined : <PlusOutlined />}
onClick={() => add(createNewRow())} onClick={() => add(createNewRow())}
size="small" size="small"
block={block} className={block ? "rb:mt-1 rb:text-[12px]! rb:bg-transparent!" : "rb:text-[12px]!"}
className={block ? "rb:mt-1" : ""}
> >
{block && `+${t('common.add')}`} {block && `+${t('common.add')}`}
</Button> </Button>
@@ -145,8 +154,8 @@ const EditableTable: React.FC<EditableTableProps> = ({
<> <>
{title && ( {title && (
<div className="rb:flex rb:items-center rb:mb-2 rb:justify-between"> <div className="rb:flex rb:items-center rb:mb-2 rb:justify-between">
<div className="rb:font-medium">{title}</div> <div className="rb:font-medium rb:text-[12px] rb:leading-4.5">{title}</div>
<AddButton /> <AddButton block={true} />
</div> </div>
)} )}
@@ -161,8 +170,9 @@ const EditableTable: React.FC<EditableTableProps> = ({
columns={getColumns(remove)} columns={getColumns(remove)}
pagination={false} pagination={false}
size="small" size="small"
rowClassName="rb:p-0! rb:bg-[#F6F8FC]!"
locale={{ emptyText: <Empty size={88} /> }} locale={{ emptyText: <Empty size={88} /> }}
scroll={{ x: 'max-content' }} style={{ width: '274px' }}
/> />
{!title && <AddButton block />} {!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 { useTranslation } from 'react-i18next'
import { Form, Row, Col, Select, Button, Divider, InputNumber, Switch, Input } from 'antd' 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 Editor from '../../Editor'
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
import AuthConfigModal from './AuthConfigModal' 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 ( return (
<> <>
<div className="rb:flex rb:items-center rb:justify-between rb:mb-4"> <div className="rb:flex rb:items-center rb:justify-between rb:mb-1">
<div>API</div> <div className="rb:font-medium rb:text-[12px] rb:leading-4.5">API</div>
<Button onClick={handleChangeAuth}>{t('workflow.config.http-request.auth')}</Button> <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> </div>
<Row gutter={16}> <Row gutter={4}>
<Col span={8}> <Col span={8}>
<Form.Item name="method"> <Form.Item name="method">
<Select <Select
@@ -85,35 +94,43 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
{ label: 'PUT', value: 'PUT' }, { label: 'PUT', value: 'PUT' },
{ label: 'DELETE', value: 'DELETE' }, { label: 'DELETE', value: 'DELETE' },
]} ]}
className="rb:bg-transparent!"
/> />
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={16}> <Col span={16}>
<Form.Item name="url"> <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> </Form.Item>
</Col> </Col>
</Row> </Row>
<Form.Item name="auth" hidden> <Form.Item name="auth" hidden>
</Form.Item> </Form.Item>
<Form.Item name="headers"> <Form.Item name="headers" noStyle>
<EditableTable <EditableTable
size="small"
parentName="headers" parentName="headers"
title="HEADERS" title="HEADERS"
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')} options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
/> />
</Form.Item> </Form.Item>
<Form.Item name="params"> <Form.Item name="params" noStyle>
<EditableTable <EditableTable
size="small"
parentName="params" parentName="params"
title="PARAMS" title="PARAMS"
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')} options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
/> />
</Form.Item> </Form.Item>
<Form.Item label="BODY"> <Form.Item label="BODY" className="rb:mb-0!">
<Form.Item name={['body', 'content_type']}> <Form.Item name={['body', 'content_type']}>
<Select <Select
placeholder={t('common.pleaseSelect')} placeholder={t('common.pleaseSelect')}
@@ -131,6 +148,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
{values?.body?.content_type === 'form-data' && {values?.body?.content_type === 'form-data' &&
<Form.Item name={['body', 'data']} noStyle> <Form.Item name={['body', 'data']} noStyle>
<EditableTable <EditableTable
size="small"
parentName={['body', 'data']} parentName={['body', 'data']}
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')} options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
typeOptions={[ typeOptions={[
@@ -143,6 +161,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
{values?.body?.content_type === 'x-www-form-urlencoded' && {values?.body?.content_type === 'x-www-form-urlencoded' &&
<Form.Item name={['body', 'data']} noStyle> <Form.Item name={['body', 'data']} noStyle>
<EditableTable <EditableTable
size="small"
parentName={['body', 'data']} parentName={['body', 'data']}
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')} options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
filterBooleanType={true} filterBooleanType={true}
@@ -150,7 +169,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
</Form.Item> </Form.Item>
} }
{values?.body?.content_type === 'json' && {values?.body?.content_type === 'json' &&
<Form.Item name={['body', 'data']}> <Form.Item name={['body', 'data']} noStyle>
<MessageEditor <MessageEditor
key="json" key="json"
parentName={['body', 'data']} parentName={['body', 'data']}
@@ -161,7 +180,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
</Form.Item> </Form.Item>
} }
{values?.body?.content_type === 'raw' && {values?.body?.content_type === 'raw' &&
<Form.Item name={['body', 'data']}> <Form.Item name={['body', 'data']} noStyle>
<MessageEditor <MessageEditor
key="raw" key="raw"
parentName={['body', 'data']} parentName={['body', 'data']}
@@ -172,7 +191,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
</Form.Item> </Form.Item>
} }
{values?.body?.content_type === 'binary' && {values?.body?.content_type === 'binary' &&
<Form.Item name={['body', 'data']}> <Form.Item name={['body', 'data']} noStyle>
<VariableSelect <VariableSelect
placeholder={t('common.pleaseSelect')} placeholder={t('common.pleaseSelect')}
options={options.filter(vo => vo.dataType.includes('file'))} options={options.filter(vo => vo.dataType.includes('file'))}
@@ -182,15 +201,19 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
} }
</Form.Item> </Form.Item>
<Divider /> <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 /> <Switch />
</Form.Item> </Form.Item>
<Divider /> <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 <Form.Item
name={['timeouts', 'connect_timeout']} name={['timeouts', 'connect_timeout']}
label={t('workflow.config.http-request.connect_timeout')} label={t('workflow.config.http-request.connect_timeout')}
hidden={collapsed}
> >
<InputNumber <InputNumber
placeholder={t('common.pleaseEnter')} placeholder={t('common.pleaseEnter')}
@@ -201,6 +224,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
<Form.Item <Form.Item
name={['timeouts', 'read_timeout']} name={['timeouts', 'read_timeout']}
label={t('workflow.config.http-request.read_timeout')} label={t('workflow.config.http-request.read_timeout')}
hidden={collapsed}
> >
<InputNumber <InputNumber
placeholder={t('common.pleaseEnter')} placeholder={t('common.pleaseEnter')}
@@ -211,6 +235,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
<Form.Item <Form.Item
name={['timeouts', 'write_timeout']} name={['timeouts', 'write_timeout']}
label={t('workflow.config.http-request.write_timeout')} label={t('workflow.config.http-request.write_timeout')}
hidden={collapsed}
> >
<InputNumber <InputNumber
placeholder={t('common.pleaseEnter')} placeholder={t('common.pleaseEnter')}

View File

@@ -1,6 +1,6 @@
import { type FC, useRef, useState, useEffect } from 'react' import { type FC, useRef, useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next' 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 knowledgeEmpty from '@/assets/images/application/knowledgeEmpty.svg'
import type { import type {
KnowledgeConfigForm, KnowledgeConfigForm,
@@ -113,49 +113,65 @@ const Knowledge: FC<{value?: KnowledgeConfig; onChange?: (config: KnowledgeConfi
} }
return ( return (
<div> <div>
<div className="rb:flex rb:justify-between rb:items-center"> <div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
<div>{t('application.knowledgeBaseAssociation')}</div> <div className="rb:text-[12px] rb:font-medium rb:leading-4.5">
{t('application.knowledgeBaseAssociation')}
</div>
<Space> <Button
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handleKnowledgeConfig}>{t('workflow.config.knowledge-retrieval.recallConfig')}</Button> onClick={handleKnowledgeConfig}
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handleAddKnowledge}>+</Button> className="rb:py-0! rb:px-1! rb:text-[12px]! rb:group rb:gap-0.5!"
</Space> 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> </div>
{knowledgeList.length === 0 <Space size={10} direction="vertical" className="rb:w-full!">
? <Empty url={knowledgeEmpty} size={88} subTitle={t('application.knowledgeEmpty')} /> <Button
: type="dashed"
<List block
grid={{ gutter: 12, column: 1 }} size="middle"
dataSource={knowledgeList} className="rb:text-[12px]!"
renderItem={(item) => { onClick={handleAddKnowledge}
if (!item.id) return null >
return ( + {t('workflow.config.knowledge-retrieval.addKnowledge')}
<List.Item> </Button>
<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"> {knowledgeList.length === 0
{item.name} ? <Empty url={knowledgeEmpty} size={88} subTitle={t('application.knowledgeEmpty')} />
<Tag color={item.status === 1 ? 'success' : item.status === 0 ? 'default' : 'error'} className="rb:ml-2"> : knowledgeList.map(item => {
{item.status === 1 ? t('common.enable') : item.status === 0 ? t('common.disabled') : t('common.deleted')} if (!item.id) return null
</Tag> return (
<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 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> <div className="">
<Space size={12}> <span className="rb:font-medium rb:leading-4">{item.name}</span>
<div <Tag
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')]" color={item.status === 1 ? 'success' : item.status === 0 ? 'default' : 'error'}
onClick={() => handleEditKnowledge(item)} className="rb:ml-1 rb:py-0! rb:px-1! rb:text-[12px] rb:leading-3.5!"
></div> >
<div {item.status === 1 ? t('common.enable') : item.status === 0 ? t('common.disabled') : t('common.deleted')}
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')]" </Tag>
onClick={() => handleDeleteKnowledge(item.id)} <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> </div>
</Space> <Space size={12}>
</div> <div
</List.Item> 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 <KnowledgeGlobalConfigModal
data={editConfig} data={editConfig}

View File

@@ -83,6 +83,7 @@ const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfig
<Form <Form
form={form} form={form}
layout="vertical" layout="vertical"
size="middle"
> >
{data && ( {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]"> <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={form} form={form}
layout="vertical" layout="vertical"
size="middle"
> >
<div className="rb:text-[#5B6167] rb:mb-6">{t('application.globalConfigDesc')}</div> <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')} placeholder={t('knowledgeBase.searchPlaceholder')}
onSearch={handleSearch} onSearch={handleSearch}
style={{ width: '100%' }} style={{ width: '100%' }}
size="middle"
/> />
{filterList.length === 0 {filterList.length === 0
? <Empty /> ? <Empty />

View File

@@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { MinusCircleOutlined } from '@ant-design/icons'; import { Button, Form, Input, Divider } from 'antd';
import { Button, Form, Input, Space, Row, Col } from 'antd';
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
import VariableSelect from '../VariableSelect' import VariableSelect from '../VariableSelect'
@@ -16,43 +15,55 @@ const MappingList: React.FC<MappingListProps> = ({ name, options }) => {
<Form.List name={name}> <Form.List name={name}>
{(fields, { add, remove }) => ( {(fields, { add, remove }) => (
<> <>
{fields.map(({ key, name, ...restField }) => ( <div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
<Row key={key} gutter={12} className="rb:mb-2"> <div className="rb:text-[12px] rb:font-medium rb:leading-4.5">
<Col span={10}> {t('workflow.config.jinja-render.mapping')}
<Form.Item </div>
{...restField}
name={[name, 'name']} <Button
noStyle onClick={() => add()}
> className="rb:py-0! rb:px-1! rb:text-[12px]!"
<Input placeholder={t('common.pleaseEnter')} data-field-type="mapping-name" /> size="small"
</Form.Item> >
</Col> + {t('workflow.config.addVariable')}
<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')}
</Button> </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> </Form.List>
<Divider />
</> </>
) )
}; };

View File

@@ -30,7 +30,7 @@ const MemoryConfig: FC<{ options: Suggestion[]; parentName: string; }> = ({
return ( return (
<> <>
{values?.memory?.enable && <> {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')} {t('workflow.config.llm.memory')}
<span>{t('workflow.config.llm.inner')}</span> <span>{t('workflow.config.llm.inner')}</span>
</div> </div>
@@ -40,6 +40,7 @@ const MemoryConfig: FC<{ options: Suggestion[]; parentName: string; }> = ({
isArray={false} isArray={false}
parentName={[parentName, 'messages']} parentName={[parentName, 'messages']}
options={options} options={options}
size="small"
/> />
</Form.Item> </Form.Item>

View File

@@ -1,13 +1,14 @@
import { type FC, useMemo } from 'react'; import { type FC, useMemo } from 'react';
import clsx from 'clsx'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Input, Form, Space, Button, Row, Col, Select, type FormListOperation } from 'antd'; import { Input, Form, Space, Button, Row, Col, Select, type FormListOperation } from 'antd';
import { MinusCircleOutlined } from '@ant-design/icons';
import Editor from '../Editor' import Editor from '../Editor'
import type { Suggestion } from '../Editor/plugin/AutocompletePlugin' import type { Suggestion } from '../Editor/plugin/AutocompletePlugin'
interface MessageEditor { interface MessageEditor {
options: Suggestion[]; options: Suggestion[];
title?: string title?: string;
titleVariant?: 'outlined' | 'borderless';
isArray?: boolean; isArray?: boolean;
parentName?: string | string[]; parentName?: string | string[];
label?: string; label?: string;
@@ -15,6 +16,7 @@ interface MessageEditor {
value?: string; value?: string;
enableJinja2?: boolean; enableJinja2?: boolean;
onChange?: (value?: string) => void; onChange?: (value?: string) => void;
size?: 'small' | 'default'
} }
const roleOptions = [ const roleOptions = [
// { label: 'SYSTEM', value: 'SYSTEM' }, // { label: 'SYSTEM', value: 'SYSTEM' },
@@ -23,11 +25,13 @@ const roleOptions = [
] ]
const MessageEditor: FC<MessageEditor> = ({ const MessageEditor: FC<MessageEditor> = ({
title, title,
titleVariant = 'outlined',
isArray = true, isArray = true,
parentName = 'messages', parentName = 'messages',
placeholder, placeholder,
options, options,
enableJinja2 = false, enableJinja2 = false,
size = 'default'
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const form = Form.useFormInstance(); const form = Form.useFormInstance();
@@ -74,14 +78,16 @@ const MessageEditor: FC<MessageEditor> = ({
if (!isArray) { if (!isArray) {
return ( 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> <Row>
<Col span={12}> <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> </Col>
</Row> </Row>
<Form.Item name={parentName} noStyle> <Form.Item name={parentName} noStyle>
<Editor enableJinja2={enableJinja2} placeholder={placeholder} options={processedOptions} /> <Editor size={size} enableJinja2={enableJinja2} placeholder={placeholder} options={processedOptions} />
</Form.Item> </Form.Item>
</Space> </Space>
); );
@@ -90,7 +96,7 @@ const MessageEditor: FC<MessageEditor> = ({
return ( return (
<Form.List name={parentName}> <Form.List name={parentName}>
{(fields, { add, remove }) => ( {(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 }) => { {fields.map(({ key, name, ...restField }) => {
const fieldValue = Array.isArray(parentName) const fieldValue = Array.isArray(parentName)
? parentName.reduce((obj, key) => obj?.[key], values) ? parentName.reduce((obj, key) => obj?.[key], values)
@@ -99,16 +105,17 @@ const MessageEditor: FC<MessageEditor> = ({
const currentRole = (fieldValue?.[name]?.role || 'USER').toUpperCase(); const currentRole = (fieldValue?.[name]?.role || 'USER').toUpperCase();
return ( 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> <Row>
<Col span={12}> <Col span={12}>
<Form.Item {...restField} name={[name, 'role']} noStyle> <Form.Item {...restField} name={[name, 'role']} noStyle>
{currentRole === 'SYSTEM' ? ( {currentRole === 'SYSTEM' ? (
<Input disabled /> <Input disabled className="rb:font-medium!" />
) : ( ) : (
<Select <Select
options={roleOptions} options={roleOptions}
disabled={currentRole === 'SYSTEM'} disabled={currentRole === 'SYSTEM'}
className="rb:font-medium!"
/> />
)} )}
</Form.Item> </Form.Item>
@@ -116,20 +123,23 @@ const MessageEditor: FC<MessageEditor> = ({
{currentRole !== 'SYSTEM' && ( {currentRole !== 'SYSTEM' && (
<Col span={12}> <Col span={12}>
<div className="rb:h-full rb:flex rb:justify-end rb:items-center"> <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> </div>
</Col> </Col>
)} )}
</Row> </Row>
<Form.Item {...restField} name={[name, 'content']} noStyle> <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> </Form.Item>
</Space> </Space>
); );
})} })}
<Form.Item noStyle> <Form.Item noStyle>
<Button type="dashed" onClick={() => handleAdd(add)} block> <Button type="dashed" size="middle" className="rb:text-[12px]!" onClick={() => handleAdd(add)} block>
+{t('workflow.addMessage')} + {t('workflow.addMessage')}
</Button> </Button>
</Form.Item> </Form.Item>
</Space> </Space>

View File

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

View File

@@ -1,7 +1,7 @@
import { type FC, useRef } from 'react' import { type FC, useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button, Space, List } from 'antd' import { Button, Space } from 'antd'
import Empty from '@/components/Empty'
import type { ParamItem, ParamEditModalRef } from './types' import type { ParamItem, ParamEditModalRef } from './types'
import ParamEditModal from './ParamEditModal' import ParamEditModal from './ParamEditModal'
@@ -41,47 +41,37 @@ const ParamsList: FC<ParamsListProps> = ({
} }
return ( return (
<div> <div>
<div className="rb:flex rb:justify-between rb:items-center"> <div className="rb:leading-4.25 rb:text-[12px] rb:font-medium rb:mb-2">
<div>{label}</div> {label}
<Space>
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handleAdd}>+</Button>
</Space>
</div> </div>
{value?.length === 0 <Space size={10} direction="vertical" className="rb:w-full!">
? <Empty size={88} /> <Button type="dashed" block size="middle" className="rb:text-[12px]!" onClick={handleAdd}>+ {t('workflow.config.parameter-extractor.addParams')}</Button>
:
<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>
</div> {value?.map((item, index) => (
<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> <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"> key={index}
<div 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"
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> <span className="rb:font-medium">{item.name}</span>
<div <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>
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')]" <div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4.25 rb:mt-0.5">{item.desc}</div>
onClick={() => handleDelete(index)} </div>
></div>
</Space> <Space size={8}>
</div> <div
</List.Item> 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 <ParamEditModal
ref={paramEditModalRef} 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')} label={t('workflow.config.tool.tool_id')}
> >
<Cascader <Cascader
placeholder={t('common.pleaseSelect')}
options={optionList} options={optionList}
loadData={loadData} loadData={loadData}
onChange={handleChange} onChange={handleChange}
@@ -187,28 +188,30 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({
label={parameter.name} label={parameter.name}
extra={parameter.type === 'boolean' ? undefined : parameter.description} extra={parameter.type === 'boolean' ? undefined : parameter.description}
rules={[ rules={[
{ required: parameter.required, message: t('workflow.config.tool.required') } { required: parameter.required, message: t('common.pleaseEnter') }
]} ]}
layout={parameter.type === 'boolean' ? 'horizontal' : 'vertical'} layout={parameter.type === 'boolean' ? 'horizontal' : 'vertical'}
className={parameter.type === 'boolean' ? 'rb:mb-0!' : ''} className={parameter.type === 'boolean' ? 'rb:mb-0!' : ''}
> >
{parameter.type === 'string' && parameter.enum && parameter.enum.length > 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' : parameter.type === 'boolean'
? <Switch /> ? <Switch size="small" />
: parameter.type === 'integer' || parameter.type === 'number' : parameter.type === 'integer' || parameter.type === 'number'
? <InputNumber ? <InputNumber
min={parameter.minimum} min={parameter.minimum}
max={parameter.maximum} max={parameter.maximum}
step={parameter.type === 'integer' ? 1 : 0.01} step={parameter.type === 'integer' ? 1 : 0.01}
placeholder={t('common.pleaseEnter')} placeholder={t('common.pleaseEnter')}
className="rb:w-full!" className="rb:w-full!"
size="small"
onChange={(value) => form.setFieldValue(['tool_parameters', parameter.name], value)} onChange={(value) => form.setFieldValue(['tool_parameters', parameter.name], value)}
/> />
: <Editor : <Editor
height={32} height={32}
variant="outlined" variant="outlined"
options={options} options={options}
size="small"
placeholder={t('common.pleaseEnter')} 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 { Form, Input, Select, InputNumber, Checkbox, Tag } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { StartVariableItem, VariableEditModalRef } from '../../types' import type { Variable, VariableEditModalRef } from './types'
import RbModal from '@/components/RbModal' import RbModal from '@/components/RbModal'
import SortableList from '@/components/SortableList' import SortableList from '@/components/SortableList'
const FormItem = Form.Item; const FormItem = Form.Item;
interface VariableEditModalProps { interface VariableEditModalProps {
refresh: (values: StartVariableItem) => void; refresh: (values: Variable) => void;
} }
const types = [ const types = [
@@ -36,9 +36,9 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
}, ref) => { }, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [form] = Form.useForm<StartVariableItem>(); const [form] = Form.useForm<Variable>();
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [editVo, setEditVo] = useState<StartVariableItem | null>(null) const [editVo, setEditVo] = useState<Variable | null>(null)
const values = Form.useWatch([], form); const values = Form.useWatch([], form);
@@ -50,7 +50,7 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
setEditVo(null) setEditVo(null)
}; };
const handleOpen = (variable?: StartVariableItem) => { const handleOpen = (variable?: Variable) => {
setVisible(true); setVisible(true);
if (variable) { if (variable) {
setEditVo(variable || null) setEditVo(variable || null)
@@ -85,7 +85,7 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
return ( return (
<RbModal <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} open={visible}
onCancel={handleClose} onCancel={handleClose}
okText={t('common.save')} okText={t('common.save')}
@@ -96,6 +96,7 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
form={form} form={form}
layout="vertical" layout="vertical"
initialValues={initialValues} initialValues={initialValues}
size="middle"
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }} 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; onChange?: (value: string) => void;
allowClear?: boolean; allowClear?: boolean;
filterBooleanType?: boolean; filterBooleanType?: boolean;
size?: 'small' | 'middle' | 'large'
} }
const VariableSelect: FC<VariableSelectProps> = ({ const VariableSelect: FC<VariableSelectProps> = ({
@@ -18,7 +19,7 @@ const VariableSelect: FC<VariableSelectProps> = ({
value, value,
allowClear = true, allowClear = true,
onChange, onChange,
size, size = 'middle',
filterBooleanType = false, filterBooleanType = false,
...resetPorps ...resetPorps
}) => { }) => {

View File

@@ -1,13 +1,13 @@
import { type FC, useEffect, useState, useRef, useMemo } from "react"; import { type FC, useEffect, useState, useRef, useMemo } from "react";
import clsx from 'clsx'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Graph, Node } from '@antv/x6'; 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 Empty from '@/components/Empty';
import emptyIcon from '@/assets/images/workflow/empty.png' import emptyIcon from '@/assets/images/workflow/empty.png'
import CustomSelect from "@/components/CustomSelect"; import CustomSelect from "@/components/CustomSelect";
import VariableEditModal from './VariableEditModal';
import MessageEditor from './MessageEditor' import MessageEditor from './MessageEditor'
import Knowledge from './Knowledge/Knowledge'; import Knowledge from './Knowledge/Knowledge';
import type { Suggestion } from '../Editor/plugin/AutocompletePlugin' import type { Suggestion } from '../Editor/plugin/AutocompletePlugin'
@@ -23,7 +23,11 @@ import CycleVarsList from './CycleVarsList'
import AssignmentList from './AssignmentList' import AssignmentList from './AssignmentList'
import ToolConfig from './ToolConfig' import ToolConfig from './ToolConfig'
import MemoryConfig from './MemoryConfig' import MemoryConfig from './MemoryConfig'
import VariableList from './VariableList'
// import { calculateVariableList } from './utils/variableListCalculator' // import { calculateVariableList } from './utils/variableListCalculator'
import styles from './properties.module.css'
import Editor from "../Editor";
import RbSlider from './RbSlider'
interface PropertiesProps { interface PropertiesProps {
selectedNode?: Node | null; selectedNode?: Node | null;
@@ -42,12 +46,9 @@ const Properties: FC<PropertiesProps> = ({
chatVariables chatVariables
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { modal } = App.useApp()
const [form] = Form.useForm<NodeConfig>(); const [form] = Form.useForm<NodeConfig>();
const [configs, setConfigs] = useState<Record<string,NodeConfig>>({} as Record<string,NodeConfig>) const [configs, setConfigs] = useState<Record<string,NodeConfig>>({} as Record<string,NodeConfig>)
const values = Form.useWatch([], form); const values = Form.useWatch([], form);
const variableModalRef = useRef<VariableEditModalRef>(null)
const [editIndex, setEditIndex] = useState<number | null>(null)
const [graphUpdateTrigger, setGraphUpdateTrigger] = useState(0) const [graphUpdateTrigger, setGraphUpdateTrigger] = useState(0)
const prevMappingNamesRef = useRef<string[]>([]) const prevMappingNamesRef = useRef<string[]>([])
const prevTemplateVarsRef = useRef<string[]>([]) const prevTemplateVarsRef = useRef<string[]>([])
@@ -243,49 +244,6 @@ const Properties: FC<PropertiesProps> = ({
} }
}, [values, selectedNode, form]) }, [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(() => { const variableList = useMemo(() => {
if (!selectedNode || !graphRef?.current) return []; if (!selectedNode || !graphRef?.current) return [];
@@ -586,7 +544,7 @@ const Properties: FC<PropertiesProps> = ({
break break
case 'question-classifier': case 'question-classifier':
const classNameKey = `${dataNodeId}_class_name`; const classNameKey = `${dataNodeId}_class_name`;
const outputKey = `${dataNodeId}_output`; // const outputKey = `${dataNodeId}_output`;
if (!addedKeys.has(classNameKey)) { if (!addedKeys.has(classNameKey)) {
addedKeys.add(classNameKey); addedKeys.add(classNameKey);
variableList.push({ variableList.push({
@@ -1039,11 +997,11 @@ const Properties: FC<PropertiesProps> = ({
console.log('variableList', variableList) console.log('variableList', variableList)
return ( return (
<div className="rb:w-75 rb:fixed rb:right-0 rb:top-16 rb:bottom-0 rb:p-3"> <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:mb-3">{t('workflow.nodeProperties')}</div> <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 {!selectedNode
? <Empty url={emptyIcon} size={140} className="rb:h-full rb:mx-15" title={t('workflow.empty')} /> ? <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')}> <Form.Item name="name" label={t('workflow.nodeName')}>
<Input <Input
placeholder={t('common.pleaseEnter')} placeholder={t('common.pleaseEnter')}
@@ -1073,46 +1031,13 @@ const Properties: FC<PropertiesProps> = ({
if (selectedNode?.data?.type === 'start' && key === 'variables' && config.type === 'define') { if (selectedNode?.data?.type === 'start' && key === 'variables' && config.type === 'define') {
return ( return (
<div key={key}> <Form.Item key={key} name={key}>
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2.75"> <VariableList
<div className="rb:leading-5"> parentName={key}
{t(`workflow.config.${selectedNode?.data?.type}.${key}`)} selectedNode={selectedNode}
</div> config={config}
<Button style={{padding: '0 8px', height: '24px'}} onClick={handleAddVariable}>+{t('application.addVariables')}</Button> />
</div> </Form.Item>
<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>
) )
} }
@@ -1143,23 +1068,12 @@ const Properties: FC<PropertiesProps> = ({
key={key} key={key}
options={contextVariableList.filter(variable => variable.nodeData?.type !== 'knowledge-retrieval')} options={contextVariableList.filter(variable => variable.nodeData?.type !== 'knowledge-retrieval')}
parentName={key} parentName={key}
placeholder={t(config.placeholder || 'common.pleaseSelect')}
size="small"
/> />
</Form.Item> </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') { if (config.type === 'define') {
return null return null
} }
@@ -1184,6 +1098,8 @@ const Properties: FC<PropertiesProps> = ({
parentName={key} parentName={key}
enableJinja2={config.enableJinja2 as boolean} enableJinja2={config.enableJinja2 as boolean}
options={getFilteredVariableList(selectedNode?.data?.type, key)} options={getFilteredVariableList(selectedNode?.data?.type, key)}
titleVariant={config.titleVariant}
size="small"
/> />
</Form.Item> </Form.Item>
) )
@@ -1206,9 +1122,9 @@ const Properties: FC<PropertiesProps> = ({
name={key} name={key}
options={getFilteredVariableList(selectedNode?.data?.type, key)} options={getFilteredVariableList(selectedNode?.data?.type, key)}
isCanAdd={!!(values as any)?.group} isCanAdd={!!(values as any)?.group}
size="small"
/> />
</Form.Item> </Form.Item>
) )
} }
if (config.type === 'caseList') { if (config.type === 'caseList') {
@@ -1226,9 +1142,7 @@ const Properties: FC<PropertiesProps> = ({
if (config.type === 'mappingList') { if (config.type === 'mappingList') {
return ( return (
<Form.Item key={key} name={key} <Form.Item key={key} name={key} noStyle>
label={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
>
<MappingList name={key} options={getFilteredVariableList(selectedNode?.data?.type, key)} /> <MappingList name={key} options={getFilteredVariableList(selectedNode?.data?.type, key)} />
</Form.Item> </Form.Item>
@@ -1238,6 +1152,7 @@ const Properties: FC<PropertiesProps> = ({
return ( return (
<Form.Item key={key} name={key}> <Form.Item key={key} name={key}>
<CycleVarsList <CycleVarsList
size="small"
parentName={key} parentName={key}
options={getFilteredVariableList(selectedNode?.data?.type, key)} options={getFilteredVariableList(selectedNode?.data?.type, key)}
/> />
@@ -1276,87 +1191,14 @@ const Properties: FC<PropertiesProps> = ({
</Form.Item> </Form.Item>
) )
} }
if (config.type === 'conditionList') {
return ( return (
<Form.Item <Form.Item
key={key} key={key}
name={key} name={key}
label={t(`workflow.config.${selectedNode?.data?.type}.${key}`)} noStyle
layout={config.type === 'switch' ? 'horizontal' : 'vertical'} >
> <ConditionList
{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
parentName={key} parentName={key}
options={(() => { options={(() => {
const cycleVars = values?.cycle_vars || []; const cycleVars = values?.cycle_vars || [];
@@ -1375,6 +1217,93 @@ const Properties: FC<PropertiesProps> = ({
graphRef={graphRef} graphRef={graphRef}
addBtnText={t('workflow.config.addCase')} 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 : null
} }
</Form.Item> </Form.Item>
@@ -1383,11 +1312,6 @@ const Properties: FC<PropertiesProps> = ({
} }
</Form> </Form>
} }
<VariableEditModal
ref={variableModalRef}
refresh={handleRefreshVariable}
/>
</div> </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, type: "end", icon: endIcon,
config: { config: {
output: { output: {
type: 'define' type: 'editor'
} }
} }
}, },
@@ -125,6 +125,7 @@ export const nodeLibrary: NodeLibrary[] = [
}, },
context: { context: {
type: 'variableList', type: 'variableList',
placeholder: 'workflow.config.llm.contextPlaceholder'
}, },
messages: { messages: {
type: 'define', type: 'define',
@@ -134,7 +135,8 @@ export const nodeLibrary: NodeLibrary[] = [
content: undefined, content: undefined,
readonly: true readonly: true
}, },
] ],
placeholder: 'workflow.config.llm.messagesPlaceholder'
}, },
memory: { memory: {
type: 'memoryConfig', type: 'memoryConfig',
@@ -170,7 +172,8 @@ export const nodeLibrary: NodeLibrary[] = [
}, },
text: { text: {
type: 'variableList', type: 'variableList',
filterLoopIterationVars: true filterLoopIterationVars: true,
placeholder: 'workflow.config.parameter-extractor.textPlaceholder'
}, },
params: { params: {
type: 'paramList', type: 'paramList',
@@ -178,6 +181,8 @@ export const nodeLibrary: NodeLibrary[] = [
prompt: { prompt: {
type: 'messageEditor', type: 'messageEditor',
isArray: false, isArray: false,
titleVariant: 'borderless',
placeholder: 'workflow.config.parameter-extractor.promptPlaceholder'
}, },
} }
} }
@@ -189,7 +194,7 @@ export const nodeLibrary: NodeLibrary[] = [
{ type: "memory-read", icon: memoryReadIcon, { type: "memory-read", icon: memoryReadIcon,
config: { config: {
message: { message: {
type: 'messageEditor', type: 'editor',
isArray: false isArray: false
}, },
config_id: { config_id: {
@@ -212,7 +217,7 @@ export const nodeLibrary: NodeLibrary[] = [
{ type: "memory-write", icon: memoryWriteIcon, { type: "memory-write", icon: memoryWriteIcon,
config: { config: {
message: { message: {
type: 'messageEditor', type: 'editor',
isArray: false isArray: false
}, },
config_id: { config_id: {
@@ -270,7 +275,8 @@ export const nodeLibrary: NodeLibrary[] = [
}, },
user_supplement_prompt: { user_supplement_prompt: {
type: 'messageEditor', type: 'messageEditor',
isArray: false isArray: false,
titleVariant: 'borderless'
} }
} }
}, },
@@ -436,6 +442,7 @@ export const nodeLibrary: NodeLibrary[] = [
type: 'messageEditor', type: 'messageEditor',
isArray: false, isArray: false,
enableJinja2: true, enableJinja2: true,
titleVariant: 'borderless',
defaultValue: "{{arg1}}" defaultValue: "{{arg1}}"
}, },
} }

View File

@@ -1,8 +1,11 @@
import { Graph } from '@antv/x6'; import { Graph } from '@antv/x6';
import type { KnowledgeConfig } from './components/Properties/Knowledge/types' import type { KnowledgeConfig } from './components/Properties/Knowledge/types'
import type { Variable } from './components/Properties/VariableList/types'
export interface NodeConfig { export interface NodeConfig {
type: 'input' | 'textarea' | 'select' | 'inputNumber' | 'slider' | 'customSelect' | 'define' | 'knowledge' | 'variableList' | string; type: 'input' | 'textarea' | 'select' | 'inputNumber' | 'slider' | 'customSelect' | 'define' | 'knowledge' | 'variableList' | string;
placeholder?: string;
titleVariant?: 'outlined' | 'borderless';
options?: { label: string; value: string }[]; options?: { label: string; value: string }[];
max?: number; max?: number;
@@ -14,7 +17,7 @@ export interface NodeConfig {
valueKey?: string; valueKey?: string;
labelKey?: string; labelKey?: string;
defaultValue?: any | StartVariableItem[]; defaultValue?: any;
sys?: Array<{ sys?: Array<{
name: string; name: string;
@@ -37,6 +40,7 @@ export interface NodeProperties {
id?: string; id?: string;
config?: Record<string, NodeConfig>; config?: Record<string, NodeConfig>;
hidden?: boolean; hidden?: boolean;
cycle?: string;
} }
export interface NodeLibrary { export interface NodeLibrary {
@@ -87,27 +91,12 @@ export interface WorkflowConfig {
updated_at: number; updated_at: number;
} }
export interface VariableEditModalRef {
handleOpen: (values?: StartVariableItem) => void;
}
export interface StartVariableItem {
name: string;
type: string;
required: boolean;
description: string;
max_length?: number;
default?: string;
readonly?: boolean;
defaultValue?: any;
value?: any;
}
export interface ChatRef { export interface ChatRef {
handleOpen: () => void; handleOpen: () => void;
} }
export type GraphRef = React.MutableRefObject<Graph | undefined> export type GraphRef = React.MutableRefObject<Graph | undefined>
export interface VariableConfigModalRef { export interface VariableConfigModalRef {
handleOpen: (values: StartVariableItem[]) => void; handleOpen: (values: Variable[]) => void;
} }
export interface ChatVariable { export interface ChatVariable {