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}
)
return fail(
BizCode.RESOURCE_NOT_FOUND,
BizCode.NOT_FOUND,
"建议缓存不存在或已过期,请调用 /generate_suggestions 接口生成新建议",
None
""
)
api_logger.info(
@@ -267,7 +267,7 @@ async def generate_emotion_suggestions(
"""生成个性化情绪建议调用LLM并缓存
Args:
request: 包含 group_id、可选的 config_id 和 force_refresh
request: 包含 end_user_id
db: 数据库会话
current_user: 当前用户
@@ -275,47 +275,22 @@ async def generate_emotion_suggestions(
新生成的个性化情绪建议响应
"""
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(
f"用户 {current_user.username} 请求生成个性化情绪建议",
extra={
"group_id": request.group_id,
"config_id": config_id
"end_user_id": request.end_user_id
}
)
# 调用服务层生成建议
data = await emotion_service.generate_emotion_suggestions(
end_user_id=request.group_id,
end_user_id=request.end_user_id,
db=db
)
# 保存到缓存
await emotion_service.save_suggestions_cache(
end_user_id=request.group_id,
end_user_id=request.end_user_id,
suggestions_data=data,
db=db,
expires_hours=24
@@ -324,7 +299,7 @@ async def generate_emotion_suggestions(
api_logger.info(
"个性化建议生成成功",
extra={
"group_id": request.group_id,
"end_user_id": request.end_user_id,
"suggestions_count": len(data.get("suggestions", []))
}
)
@@ -334,7 +309,7 @@ async def generate_emotion_suggestions(
except Exception as e:
api_logger.error(
f"生成个性化建议失败: {str(e)}",
extra={"group_id": request.group_id},
extra={"end_user_id": request.end_user_id},
exc_info=True
)
raise HTTPException(

View File

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

View File

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

View File

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

View File

@@ -243,6 +243,33 @@ class QWenCV(GptV4):
tmp_path = tmp.name
video_path = f"file://{tmp_path}"
prompt_ch = """
你是一名专业的视频转录助手,能够将视频文件的内容转写为文本,并**精确标记每句话或每个段落对应的时间戳**(开始时间-结束时间)。\n
**任务要求**
1.输入是MP4等视频文件,解析带时间戳的文本。
2.时间戳格式为 `[HH:MM:SS.mmm]`(毫秒可选),例如 `[00:01:23.456]`。
3.时间戳需尽可能贴近实际视频的起止时间误差不超过1秒。
4.如果无法确定具体时间,请根据上下文合理估算。
5.最后总结:这段视频的内容是什么?,并用恰当的句子总结这个视频。
**示例输出**
[00:00:00.000] 今天天气真好,
[00:00:02.500] 我们一起去公园散步吧。
[00:00:05.800] 公园里的花开得非常漂亮。
这段视频的内容是关于如何在CREAMS系统中进行楼宇管理集合的编辑或删除操作。视频演示了 ..."""
prompt_en = """
You are a professional video transcription assistant, capable of transcribing the content of video files into text and **precisely marking the timestamp (start time-end time) corresponding to each sentence or paragraph**.
**Task requirements**:
1. Input is MP4 or other video files, and parse the text with timestamps.
2. The timestamp format is `[HH:MM:SS.mmm]` (milliseconds are optional), for example, `[00:01:23.456]`.
3. The timestamp should be as close as possible to the actual start and end time of the video, with an error not exceeding 1 second.
4. If the specific time cannot be determined, please make a reasonable estimation based on the context.
5. Final summary: What is the content of this video? Summarize this video in an appropriate sentence.
**Example output**:
[00:00:00.000] The weather is really nice today, [00:00:02.500] let's go for a walk in the park together.
[00:00:05.800] The flowers in the park are blooming beautifully.
The content of this video is about how to edit or delete building management collections in the CREAMS system. The video demonstrates .."""
messages = [
{
"role": "user",
@@ -252,7 +279,7 @@ class QWenCV(GptV4):
"fps": 2,
},
{
"text": "视频的内容是什么?,并且,请用恰当的句子总结这个视频。" if self.lang.lower() == "chinese" else "What is the content of the video? And please summarize this video in proper sentences.",
"text": prompt_ch if self.lang.lower() == "chinese" else prompt_en,
},
],
}

View File

@@ -60,6 +60,34 @@ class QWenSeq2txt(Base):
from dashscope import MultiModalConversation
audio_path = f"file://{audio_path}"
prompt_ch = """
你是一名专业的音频转录助手能够将MP3音频文件的内容转写为文本并**精确标记每句话或每个段落对应的时间戳**(开始时间-结束时间)。\n
**任务要求**
1.输入是MP3,解析带时间戳的文本。
2.时间戳格式为 `[HH:MM:SS.mmm]`(毫秒可选),例如 `[00:01:23.456]`。
3.时间戳需尽可能贴近实际语音的起止时间误差不超过1秒。
4.如果无法确定具体时间,请根据上下文合理估算。
5.最后总结:这段音频在说什么?
**示例输出**
[00:00:00.000] 今天天气真好,
[00:00:02.500] 我们一起去公园散步吧。
[00:00:05.800] 公园里的花开得非常漂亮。
这段音频讲述的是一个关于**“吃水不忘挖井人”**的感人故事,主 ..."""
prompt_en = """
You are a professional audio transcription assistant, capable of transcribing the content of MP3 audio files into text and **precisely marking the timestamps (start time - end time) corresponding to each sentence or paragraph**.
**Task requirements**:
1. Input is MP3, parse text with timestamps.
2. The timestamp format is `[HH:MM:SS.mmm]` (milliseconds are optional), for example, `[00:01:23.456]`.
3. The timestamp should be as close as possible to the actual start and end time of the voice, with an error not exceeding 1 second.
4. If a specific time cannot be determined, please make a reasonable estimation based on the context.
5. Final summary: What is this audio talking about?
**Example Output**:
[00:00:00.000] The weather is really nice today,
[00:00:02.500] let's go for a walk in the park together.
[00:00:05.800] The flowers in the park are blooming beautifully.
This audio tells a touching story about **"Remembering the one who dug the well when drinking water"** .."""
messages = [
{
"role": "user",
@@ -68,7 +96,7 @@ class QWenSeq2txt(Base):
"audio": audio_path
},
{
"text": "这段音频在说什么?" if self.lang.lower() == "chinese" else "What is this audio saying?",
"text": prompt_ch if self.lang.lower() == "chinese" else prompt_en,
},
],
}

View File

@@ -8,6 +8,7 @@ import logging
import uuid
from typing import Any
from langchain_core.runnables import RunnableConfig
from langgraph.graph.state import CompiledStateGraph
from app.core.workflow.graph_builder import GraphBuilder
@@ -53,11 +54,11 @@ class WorkflowExecutor:
self.edges = workflow_config.get("edges", [])
self.execution_config = workflow_config.get("execution_config", {})
self.checkpoint_config = {
"configurable": {
self.checkpoint_config = RunnableConfig(
configurable={
"thread_id": uuid.uuid4(),
}
}
)
def _prepare_initial_state(self, input_data: dict[str, Any]) -> WorkflowState:
"""准备初始状态(注入系统变量和会话变量)
@@ -214,13 +215,13 @@ class WorkflowExecutor:
return {
"status": "completed",
"output": final_output,
"variables": result.get("variables", {}),
"node_outputs": node_outputs,
"messages": result.get("messages", []),
"conversation_id": conversation_id,
"elapsed_time": elapsed_time,
"token_usage": token_usage,
"error": result.get("error"),
"variables": result.get("variables", {}),
}
def build_graph(self, stream=False) -> CompiledStateGraph:
@@ -326,11 +327,10 @@ class WorkflowExecutor:
}
# 1. 构建图
graph = self.build_graph(True)
graph = self.build_graph(stream=True)
# 2. 初始化状态(自动注入系统变量)
initial_state = self._prepare_initial_state(input_data)
# 3. Execute workflow
try:
chunk_count = 0
@@ -346,14 +346,16 @@ class WorkflowExecutor:
mode, data = event
else:
# Unexpected format, log and skip
logger.warning(f"[STREAM] Unexpected event format: {type(event)}, value: {event}")
logger.warning(f"[STREAM] Unexpected event format: {type(event)}, value: {event}"
f"- execution_id: {self.execution_id}")
continue
if mode == "custom":
# Handle custom streaming events (chunks from nodes via stream writer)
chunk_count += 1
event_type = data.get("type", "node_chunk") # "message" or "node_chunk"
logger.info(f"[CUSTOM] ✅ 收到 {event_type} #{chunk_count} from {data.get('node_id')}")
logger.info(f"[CUSTOM] ✅ 收到 {event_type} #{chunk_count} from {data.get('node_id')}"
f"- execution_id: {self.execution_id}")
yield {
"event": event_type, # "message" or "node_chunk"
"data": {
@@ -380,7 +382,8 @@ class WorkflowExecutor:
variables_sys = variables.get("sys", {})
conversation_id = input_data.get("conversation_id")
execution_id = variables_sys.get("execution_id")
logger.info(f"[DEBUG] Node starts execution: {node_name}")
logger.info(f"[NODE-START] Node starts execution: {node_name} "
f"- execution_id: {self.execution_id}")
yield {
"event": "node_start",
@@ -399,7 +402,8 @@ class WorkflowExecutor:
variables_sys = variables.get("sys", {})
conversation_id = input_data.get("conversation_id")
execution_id = variables_sys.get("execution_id")
logger.info(f"[DEBUG] Node execution completed: {node_name}")
logger.info(f"[NODE-END] Node execution completed: {node_name} "
f"- execution_id: {self.execution_id}")
yield {
"event": "node_end",
@@ -407,13 +411,15 @@ class WorkflowExecutor:
"node_id": node_name,
"conversation_id": conversation_id,
"execution_id": execution_id,
"timestamp": data.get("timestamp")
"timestamp": data.get("timestamp"),
"state": result.get("node_outputs", {}).get(node_name),
}
}
elif mode == "updates":
# Handle state updates - store final state
logger.debug(f"[UPDATES] 收到 state 更新 from {list(data.keys())}")
logger.debug(f"[UPDATES] 收到 state 更新 from {list(data.keys())} "
f"- execution_id: {self.execution_id}")
# 计算耗时
end_time = datetime.datetime.now()
@@ -421,7 +427,7 @@ class WorkflowExecutor:
result = graph.get_state(self.checkpoint_config).values
logger.info(
f"Workflow execution completed (streaming), "
f"total chunks: {chunk_count}, elapsed: {elapsed_time:.2f}s"
f"total chunks: {chunk_count}, elapsed: {elapsed_time:.2f}s, execution_id: {self.execution_id}"
)
# 发送 workflow_end 事件
@@ -449,7 +455,8 @@ class WorkflowExecutor:
}
}
def _extract_final_output(self, node_outputs: dict[str, Any]) -> str | None:
@staticmethod
def _extract_final_output(node_outputs: dict[str, Any]) -> str | None:
"""从节点输出中提取最终输出
优先级:
@@ -473,7 +480,8 @@ class WorkflowExecutor:
return None
def _aggregate_token_usage(self, node_outputs: dict[str, Any]) -> dict[str, int] | None:
@staticmethod
def _aggregate_token_usage(node_outputs: dict[str, Any]) -> dict[str, int] | None:
"""聚合所有节点的 token 使用情况
Args:

View File

@@ -25,7 +25,7 @@ class WorkflowState(TypedDict):
The state object passed between nodes in a workflow, containing messages, variables, node outputs, etc.
"""
# List of messages (append mode)
messages: list[dict[str, str]]
messages: Annotated[list[dict[str, str]], lambda x, y: y]
# Set of loop node IDs, used for assigning values in loop nodes
cycle_nodes: list

View File

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

View File

@@ -67,7 +67,9 @@ class LoopRuntime:
variables=pool.get_all_conversation_vars(),
node_outputs=pool.get_all_node_outputs(),
system_vars=pool.get_all_system_vars(),
) if variable.input_type == ValueInputType.VARIABLE else TypeTransformer.transform(variable.value, variable.type)
)
if variable.input_type == ValueInputType.VARIABLE
else TypeTransformer.transform(variable.value, variable.type)
for variable in self.typed_config.cycle_vars
}
self.state["node_outputs"][self.node_id] = {
@@ -76,7 +78,9 @@ class LoopRuntime:
variables=pool.get_all_conversation_vars(),
node_outputs=pool.get_all_node_outputs(),
system_vars=pool.get_all_system_vars(),
) if variable.input_type == ValueInputType.VARIABLE else TypeTransformer.transform(variable.value, variable.type)
)
if variable.input_type == ValueInputType.VARIABLE
else TypeTransformer.transform(variable.value, variable.type)
for variable in self.typed_config.cycle_vars
}
loopstate = WorkflowState(
@@ -171,10 +175,11 @@ class LoopRuntime:
"""
loopstate = self._init_loop_state()
loop_time = self.typed_config.max_loop
child_state = []
while self.evaluate_conditional(loopstate) and loopstate["looping"] and loop_time > 0:
logger.info(f"loop node {self.node_id}: running")
await self.graph.ainvoke(loopstate)
child_state.append(await self.graph.ainvoke(loopstate))
loop_time -= 1
logger.info(f"loop node {self.node_id}: execution completed")
return loopstate["runtime_vars"][self.node_id]
return loopstate["runtime_vars"][self.node_id] | {"__child_state": child_state}

View File

@@ -10,9 +10,8 @@ from app.core.workflow.nodes.base_node import BaseNode, WorkflowState
from app.core.workflow.nodes.knowledge import KnowledgeRetrievalNodeConfig
from app.db import get_db_read
from app.models import knowledge_model, knowledgeshare_model, ModelType
from app.repositories import knowledge_repository
from app.repositories import knowledge_repository, knowledgeshare_repository
from app.schemas.chunk_schema import RetrieveType
from app.services import knowledge_service, knowledgeshare_service
from app.services.model_service import ModelConfigService
logger = logging.getLogger(__name__)
@@ -96,7 +95,7 @@ class KnowledgeRetrievalNode(BaseNode):
filters = self._build_kb_filter(kb_ids, knowledge_model.PermissionType.Share)
share_ids = knowledge_service.knowledge_repository.get_chunked_knowledgeids(
share_ids = knowledge_repository.get_chunked_knowledgeids(
db=db,
filters=filters
)
@@ -105,7 +104,7 @@ class KnowledgeRetrievalNode(BaseNode):
filters = [
knowledgeshare_model.KnowledgeShare.target_kb_id.in_(kb_ids)
]
items = knowledgeshare_service.knowledgeshare_repository.get_source_kb_ids_by_target_kb_id(
items = knowledgeshare_repository.get_source_kb_ids_by_target_kb_id(
db=db,
filters=filters
)

View File

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

View File

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

View File

@@ -27,8 +27,6 @@ from .tool_model import (
ToolExecution, ToolType, ToolStatus, AuthType, ExecutionStatus
)
from .memory_perceptual_model import MemoryPerceptualModel
from .emotion_suggestions_cache_model import EmotionSuggestionsCache
from .implicit_memory_cache_model import ImplicitMemoryCache
__all__ = [
"Tenants",
@@ -79,6 +77,4 @@ __all__ = [
"AuthType",
"ExecutionStatus",
"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,
index=True
)
release_id = Column(
UUID(as_uuid=True),
ForeignKey("app_releases.id", ondelete="CASCADE"),
nullable=True,
index=True
)
app_id = Column(
UUID(as_uuid=True),
ForeignKey("apps.id", ondelete="CASCADE"),

View File

@@ -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):
"""生成个性化情绪建议请求"""
group_id: str = Field(..., description="ID")
config_id: Optional[int] = Field(None, description="配置ID用于指定LLM模型")
end_user_id: str = Field(..., description="终端用户ID")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -120,12 +120,9 @@ def multi_agent_config_4_app_release(release: AppRelease) -> MultiAgentConfig:
def workflow_config_4_app_release(release: AppRelease) -> WorkflowConfig:
config_dict = release.config
with get_db_read() as db:
source_config = WorkflowConfigRepository(db).get_by_app_id(release.app_id)
source_config_id = source_config.id
config = WorkflowConfig(
id=source_config_id,
id=config_dict.get("id"),
app_id=release.app_id,
nodes=config_dict.get("nodes", []),
edges=config_dict.get("edges", []),

View File

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

View File

@@ -150,9 +150,19 @@ const RbMarkdown: FC<RbMarkdownProps> = ({
)
}
// 处理键盘快捷键
const handleKeyDown = (e: React.KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'c') {
const selection = window.getSelection()
if (selection && selection.toString()) {
navigator.clipboard.writeText(selection.toString())
}
}
}
// 预览模式
return (
<div className="rb:relative">
<div className="rb:relative" onKeyDown={handleKeyDown} tabIndex={0}>
<style>{`
.html-comment {
color: #999;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { Row, Col, Form, Slider, Button, Space, message, Switch } from 'antd';
import { Row, Col, Form, Slider, Button, Space, message } from 'antd';
import { useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import RbCard from '@/components/RbCard/Card';
@@ -7,6 +7,7 @@ import strategyImpactSimulator from '@/assets/images/memory/strategyImpactSimula
import LineChart from './components/LineChart'
import { getMemoryForgetConfig, updateMemoryForgetConfig } from '@/api/memory'
import type { ConfigForm } from './types'
import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
const configList = [
{
@@ -155,26 +156,12 @@ const ForgettingEngine: React.FC = () => {
{configList.map(config => {
if (config.type === 'button') {
return (
<div key={config.key} className="rb:mb-2">
<div className="rb:flex rb:items-center rb:justify-between">
<div>
<span className="rb:text-[14px] rb:font-medium rb:leading-5">{t(`forgettingEngine.${config.key}`)}</span>
</div>
<Form.Item
name={config.name}
valuePropName="checked"
className="rb:ml-2 rb:mb-0!"
>
<Switch />
</Form.Item>
</div>
<div className="rb:flex rb:text-[12px] rb:items-center rb:justify-between rb:text-[#5B6167] rb:leading-5">
<Space size={4}>
{config.range && <span>{t(`forgettingEngine.range`)}: {config.range?.join('-')}</span>}
{config.type && <span>{t(`forgettingEngine.type`)}: {config.type}</span>}
</Space>
</div>
</div>
<SwitchFormItem
title={t(`forgettingEngine.${config.key}`)}
name={config.name}
desc={config.type && <span>{t(`forgettingEngine.type`)}: {config.type}</span>}
className="rb:mb-2"
/>
)
}
return (
@@ -191,8 +178,6 @@ const ForgettingEngine: React.FC = () => {
>
{config.type === 'decimal'
? <Slider tooltip={{ open: false }} max={config.range?.[1] || 1} min={config.range?.[0] || 0} step={config.step ?? 0.01} style={{ margin: '0' }} />
: config.type === 'button'
? <Switch />
: null
}
</Form.Item>

View File

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

View File

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

View File

@@ -121,7 +121,7 @@ const EmotionTags: FC = () => {
})}
</div>
</div>
: <Empty size={88} className="rb:h-full" />
: <Empty size={88} className="rb:h-full rb:mb-4" />
}
</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 { useParams } from 'react-router-dom'
import { Skeleton, Space, Progress } from 'antd';
@@ -20,7 +20,7 @@ interface HabitsItem {
specific_examples: string[];
}
const Habits: FC = () => {
const Habits = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => {
const { t } = useTranslation()
const { id } = useParams()
const [loading, setLoading] = useState<boolean>(false)
@@ -43,6 +43,9 @@ const Habits: FC = () => {
setLoading(false)
})
}
useImperativeHandle(ref, () => ({
handleRefresh: getData
}));
return (
<>
@@ -80,5 +83,5 @@ const Habits: FC = () => {
</RbCard>
</>
)
}
})
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 { useParams } from 'react-router-dom'
import { Skeleton, Progress } from 'antd';
@@ -23,7 +23,7 @@ interface InterestAreasItem {
art: Item;
}
const InterestAreas: FC = () => {
const InterestAreas = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => {
const { t } = useTranslation()
const { id } = useParams()
const [loading, setLoading] = useState<boolean>(false)
@@ -47,6 +47,9 @@ const InterestAreas: FC = () => {
})
}
useImperativeHandle(ref, () => ({
handleRefresh: getData
}));
return (
<RbCard
title={t('implicitDetail.interestAreas')}
@@ -70,5 +73,5 @@ const InterestAreas: FC = () => {
}
</RbCard>
)
}
})
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 { useParams } from 'react-router-dom'
import { Skeleton, Progress } from 'antd';
@@ -25,7 +25,7 @@ interface PortraitItem {
literature: Item;
}
const Portrait: FC = () => {
const Portrait = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => {
const { t } = useTranslation()
const { id } = useParams()
const [loading, setLoading] = useState<boolean>(false)
@@ -49,6 +49,9 @@ const Portrait: FC = () => {
})
}
useImperativeHandle(ref, () => ({
handleRefresh: getData
}));
return (
<RbCard
title={t('implicitDetail.portrait')}
@@ -73,5 +76,5 @@ const Portrait: FC = () => {
}
</RbCard>
)
}
})
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 { useParams } from 'react-router-dom'
import { Row, Col, Skeleton } from 'antd'
@@ -31,7 +31,7 @@ const generateCategoryColors = (categories: string[]) => {
return colors
}
const Preferences: FC = () => {
const Preferences = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => {
const { t } = useTranslation()
const { id } = useParams()
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')}</> : ''
}, [selectedWord, data, t])
useImperativeHandle(ref, () => ({
handleRefresh: getData
}));
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>
@@ -184,6 +187,6 @@ const Preferences: FC = () => {
</Row>
</>
)
}
})
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 { useParams } from 'react-router-dom'
@@ -18,7 +18,7 @@ interface Suggestions {
actionable_steps: string[];
}>;
}
const Suggestions: FC = () => {
const Suggestions = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => {
const { t } = useTranslation()
const { id } = useParams()
const [suggestions, setSuggestions] = useState<Suggestions | null>(null)
@@ -37,6 +37,9 @@ const Suggestions: FC = () => {
})
}
useImperativeHandle(ref, () => ({
handleRefresh: getSuggestionData
}));
return (
<RbCard
title={t('statementDetail.suggestions')}
@@ -64,6 +67,6 @@ const Suggestions: FC = () => {
}
</RbCard>
)
}
})
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 { Row, Col } from 'antd'
import { useParams } from 'react-router-dom'
import Preferences from '../components/Preferences'
import Portrait from '../components/Portrait'
import InterestAreas from '../components/InterestAreas'
import Habits from '../components/Habits'
import {
generateProfile,
} from '@/api/memory'
const ImplicitDetail: FC = () => {
const ImplicitDetail = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => {
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 (
<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>
<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:my-3 rb:text-[#5B6167] rb:leading-5">{t('implicitDetail.portraitSubTitle')}</div>
<Row gutter={[16, 16]} className="rb:mt-4">
<Col span={12}>
<Portrait />
<Portrait ref={portraitRef} />
</Col>
<Col span={12}>
<InterestAreas />
<InterestAreas ref={interestAreasRef} />
</Col>
</Row>
<Habits />
<Habits ref={habitsRef} />
</div>
)
}
})
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 { useParams } from 'react-router-dom'
import WordCloud from '../components/WordCloud'
import EmotionTags from '../components/EmotionTags'
import Health from '../components/Health'
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 (
<Row gutter={[16, 16]}>
<Col span={12}>
@@ -18,10 +32,10 @@ const StatementDetail: FC = () => {
</Space>
</Col>
<Col span={12}>
<Suggestions />
<Suggestions ref={suggestionsRef} />
</Col>
</Row>
)
}
})
export default StatementDetail

View File

@@ -24,6 +24,8 @@ const Detail: FC = () => {
const navigate = useNavigate()
const [name, setName] = useState<string>('')
const forgetDetailRef = useRef<{ handleRefresh: () => void }>(null)
const statementDetailRef = useRef<{ handleRefresh: () => void }>(null)
const implicitDetailRef = useRef<{ handleRefresh: () => void }>(null)
useEffect(() => {
if (!id) return
@@ -45,7 +47,17 @@ const Detail: FC = () => {
navigate(`/user-memory/detail/${id}/${key}`, { replace: true })
}
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') {
@@ -67,16 +79,16 @@ const Detail: FC = () => {
</div>
</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}>
<img src={refreshIcon} className="rb:w-4 rb:h-4" />
{t('common.refresh')}
</Button>}
/>
<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 === 'IMPLICIT_MEMORY' && <ImplicitDetail />}
{type === 'IMPLICIT_MEMORY' && <ImplicitDetail ref={implicitDetailRef} />}
{type === 'SHORT_TERM_MEMORY' && <ShortTermDetail />}
{type === 'PERCEPTUAL_MEMORY' && <PerceptualDetail />}
{type === 'EPISODIC_MEMORY' && <EpisodicDetail />}

View File

@@ -2,12 +2,13 @@ import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, InputNumber, Checkbox } from 'antd';
import { useTranslation } from 'react-i18next';
import type { StartVariableItem, VariableConfigModalRef } from '../../types'
import type { VariableConfigModalRef } from '../../types'
import type { Variable } from '../Properties/VariableList/types'
import RbModal from '@/components/RbModal'
interface VariableEditModalProps {
refresh: (values: StartVariableItem[]) => void;
variables: StartVariableItem[]
refresh: (values: Variable[]) => void;
variables: Variable[]
}
const VariableConfigModal = forwardRef<VariableConfigModalRef, VariableEditModalProps>(({
@@ -15,9 +16,9 @@ const VariableConfigModal = forwardRef<VariableConfigModalRef, VariableEditModal
}, ref) => {
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
const [form] = Form.useForm<{variables: StartVariableItem[]}>();
const [form] = Form.useForm<{variables: Variable[]}>();
const [loading, setLoading] = useState(false)
const [initialValues, setInitialValues] = useState<StartVariableItem[]>([])
const [initialValues, setInitialValues] = useState<Variable[]>([])
// 封装取消方法,添加关闭弹窗逻辑
const handleClose = () => {
@@ -26,7 +27,7 @@ const VariableConfigModal = forwardRef<VariableConfigModalRef, VariableEditModal
setLoading(false)
};
const handleOpen = (values: StartVariableItem[]) => {
const handleOpen = (values: Variable[]) => {
setVisible(true);
form.setFieldsValue({variables: values})
setInitialValues([...values])

View File

@@ -1,4 +1,4 @@
import { type FC, useState, useEffect } from 'react';
import { type FC, useState, useEffect, useMemo } from 'react';
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
@@ -25,7 +25,11 @@ interface LexicalEditorProps {
options: Suggestion[];
variant?: 'outlined' | 'borderless';
height?: number;
fontSize?: number;
lineHeight?: number;
enableJinja2?: boolean;
size?: 'default' | 'small';
type?: 'input' | 'textarea'
}
const theme = {
@@ -51,8 +55,9 @@ const Editor: FC<LexicalEditorProps> =({
onChange,
options,
variant = 'borderless',
height = 60,
enableJinja2 = false,
size = 'default',
type = 'textarea'
}) => {
const [_count, setCount] = useState(0);
@@ -94,12 +99,9 @@ const Editor: FC<LexicalEditorProps> =({
display: flex;
}
.line-numbers {
background-color: #f8f9fa;
border-right: 1px solid #e1e4e8;
color: #656d76;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 12px;
line-height: 20px;
line-height: 16px;
padding: 4px 8px;
text-align: right;
user-select: none;
@@ -142,6 +144,21 @@ const Editor: FC<LexicalEditorProps> =({
console.error(error);
},
};
const minheight = useMemo(() => {
if (type === 'input') {
return `${size === 'small' ? 26 : 30}px`
}
return `${size === 'small' ? 60 : 120}px`
}, [type, size])
const fontSize = useMemo(() => {
return `${size === 'small' ? 12 : 14}px`
}, [size])
const lineHeight = useMemo(() => {
return `${size === 'small' ? 16 : 20}px`
}, [size])
const placeHolderMinheight = useMemo(() => {
return `${size === 'small' ? 16 : 30}px`
}, [type, size])
return (
<LexicalComposer initialConfig={initialConfig}>
@@ -152,7 +169,7 @@ const Editor: FC<LexicalEditorProps> =({
<div className="editor-with-line-numbers" style={{
border: variant === 'borderless' ? 'none' : '1px solid #DFE4ED',
borderRadius: '6px',
minHeight: `${height}px`,
minHeight: minheight,
}}>
<div className="line-numbers">
<div>1</div>
@@ -160,12 +177,12 @@ const Editor: FC<LexicalEditorProps> =({
<ContentEditable
className="editor-content-with-numbers"
style={{
minHeight: `${height}px`,
padding: '4px 11px',
minHeight: minheight,
padding: '4px 0',
outline: 'none',
resize: 'none',
fontSize: '14px',
lineHeight: '20px',
fontSize: fontSize,
lineHeight: lineHeight,
border: 'none',
}}
/>
@@ -173,14 +190,14 @@ const Editor: FC<LexicalEditorProps> =({
) : (
<ContentEditable
style={{
minHeight: `${height}px`,
minHeight: minheight,
padding: variant === 'borderless' ? '0' : '4px 11px',
border: variant === 'borderless' ? 'none' : '1px solid #DFE4ED',
borderRadius: '6px',
outline: 'none',
resize: 'none',
fontSize: '14px',
lineHeight: '20px',
fontSize: fontSize,
lineHeight: lineHeight,
}}
/>
)
@@ -188,12 +205,13 @@ const Editor: FC<LexicalEditorProps> =({
placeholder={
<div
style={{
minHeight: placeHolderMinheight,
position: 'absolute',
top: variant === 'borderless' ? '0' : '6px',
left: enableJinja2 ? '59px' : (variant === 'borderless' ? '0' : '11px'),
color: '#5B6167',
fontSize: '14px',
lineHeight: '20px',
color: '#A8A9AA',
fontSize: fontSize,
lineHeight: placeHolderMinheight,
pointerEvents: 'none',
}}
>

View File

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

View File

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

View File

@@ -1,8 +1,7 @@
import { type FC } from 'react'
import clsx from 'clsx'
import { useTranslation } from 'react-i18next';
import { Form, Button, Select, Space, Row, Col, Divider, InputNumber, Radio, type SelectProps } from 'antd'
import { DeleteOutlined } from '@ant-design/icons';
import { Form, Button, Select, Space, Divider, InputNumber, Radio, type SelectProps } from 'antd'
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
import VariableSelect from '../VariableSelect'
@@ -247,37 +246,40 @@ const CaseList: FC<CaseListProps> = ({
{(conditionFields, { add: addCondition, remove: removeCondition }) => {
const logicalOperator = form.getFieldValue(name)?.[caseIndex]?.logical_operator || 'and'
return (
<div className={clsx("rb:relative rb:mb-4 rb:border rb:border-gray-200 rb:rounded rb:p-3 rb:pl-5")}>
<div className="rb:flex rb:items-center rb:justify-between rb:mb-3">
<span className="rb:font-medium">
{caseIndex === 0 ? 'IF' : 'ELIF'}<br/>
{caseFields.length > 1 && <span className="rb:text-[10px] rb:text-[#5B6167]">{`CASE ${caseIndex + 1}`}</span>}
</span>
<div className={clsx("rb:relative")}>
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
<div className="rb:text-[12px] rb:leading-4.5">
<span className="rb:font-medium ">{caseIndex === 0 ? 'IF' : 'ELIF'}</span>
{caseFields.length > 1 && <span className="rb:text-[10px] rb:text-[#5B6167]"> ({`CASE ${caseIndex + 1}`})</span>}
</div>
<Space>
<Button
type="dashed"
onClick={() => addCondition({})}
className="rb:py-0! rb:px-1! rb:text-[12px]!"
size="small"
>
+ {t('workflow.config.addCase')}
</Button>
{caseFields.length > 1 && <DeleteOutlined
className="rb:text-[12px]"
onClick={() => handleRemoveCase(removeCase, caseField.name, caseIndex)}
/>}
{caseFields.length > 1 &&
<Button
className="rb:py-0! rb:px-1! rb:text-[12px]!"
onClick={() => handleRemoveCase(removeCase, caseField.name, caseIndex)}
>
{t('common.remove')}
</Button>
}
</Space>
</div>
{conditionFields?.length > 1 &&
<>
<div className="rb:absolute rb:w-3 rb:left-2 rb:top-15 rb:bottom-6 rb:z-10 rb:border rb:border-[#DFE4ED] rb:rounded-l-md rb:border-r-0"></div>
<div className="rb:absolute rb:z-10 rb:left-0 rb:top-[50%] rb:transform-[translateY(-50%)]]">
{conditionFields?.length > 1 && <div className="rb:absolute rb:top-8 rb:bottom-4 rb:w-8.5 rb:h-[calc(100%-32px)]">
<div className="rb:absolute rb:w-2.5 rb:h-[calc(50%-30px)] rb:left-5 rb:top-4 rb:z-10 rb:border-l rb:border-t rb:border-[#DFE4ED] rb:rounded-tl-[10px] rb:border-r-0"></div>
<div className="rb:absolute rb:z-10 rb:left-0 rb:top-[calc(50%-13px)]">
<Form.Item name={[caseField.name, 'logical_operator']} noStyle >
<Button size="small" className="rb:cursor-pointer" onClick={() => handleChangeLogicalOperator(caseIndex)}>{logicalOperator}</Button>
<Button size="small" className="rb:text-[12px]! rb:py-px! rb:px-1! rb:w-8.5! rb:h-5!" onClick={() => handleChangeLogicalOperator(caseIndex)}>{logicalOperator}</Button>
</Form.Item>
</div>
</>
}
<div className="rb:absolute rb:w-2.5 rb:h-[calc(50%-30px)] rb:left-5 rb:bottom-4 rb:z-10 rb:border-l rb:border-b rb:border-[#DFE4ED] rb:rounded-bl-[10px] rb:border-r-0"></div>
</div>}
{conditionFields.map((conditionField, conditionIndex) => {
const cases = form.getFieldValue(name) || [];
const currentCase = cases[caseIndex] || {};
@@ -290,91 +292,86 @@ const CaseList: FC<CaseListProps> = ({
const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || [];
const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined;
return (
<div key={conditionField.key} className={clsx({
"rb:mb-3": conditionIndex !== conditionFields.length - 1
})}>
<div className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white">
<Row gutter={12} className="rb:mb-1">
<Col span={14}>
<Form.Item name={[conditionField.name, 'left']} noStyle>
<VariableSelect
placeholder={t('common.pleaseSelect')}
options={options}
size="small"
allowClear={false}
popupMatchSelectWidth={false}
onChange={(val) => handleLeftFieldChange(caseIndex, conditionIndex, val)}
/>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name={[conditionField.name, 'operator']} noStyle>
<Select
options={operatorList.map(vo => ({
...vo,
label: t(String(vo?.label || ''))
}))}
size="small"
popupMatchSelectWidth={false}
placeholder={t('common.pleaseSelect')}
/>
</Form.Item>
</Col>
<Col span={2}>
<DeleteOutlined
className="rb:text-[12px]"
onClick={() => removeCondition(conditionField.name)}
<div key={conditionField.key} className="rb:flex rb:items-start rb:ml-9.5 rb:mb-4">
<div className="rb:flex-1 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-md">
<div className={clsx("rb:flex rb:gap-1 rb:p-1", {
'rb:border-b rb:border-b-[#DFE4ED]': !hideRightField
})}>
<Form.Item name={[conditionField.name, 'left']} noStyle>
<VariableSelect
placeholder={t('common.pleaseSelect')}
options={options}
size="small"
allowClear={false}
popupMatchSelectWidth={false}
onChange={(val) => handleLeftFieldChange(caseIndex, conditionIndex, val)}
className="rb:bg-white! rb:w-29.5!"
/>
</Col>
</Row>
</Form.Item>
<Form.Item name={[conditionField.name, 'operator']} noStyle>
<Select
options={operatorList.map(vo => ({
...vo,
label: t(String(vo?.label || ''))
}))}
size="small"
popupMatchSelectWidth={false}
placeholder={t('common.pleaseSelect')}
className="rb:bg-white! rb:w-22!"
/>
</Form.Item>
</div>
{!hideRightField && <>
{!hideRightField && <div className="rb:p-1">
{leftFieldType === 'number'
? <Row>
<Col span={12}>
<Form.Item name={[conditionField.name, 'input_type']} noStyle>
<Select
? <div className="rb:flex rb:items-center">
<Form.Item name={[conditionField.name, 'input_type']} noStyle>
<Select
placeholder={t('common.pleaseSelect')}
options={[{ value: 'Variable', label: 'Variable' }, { value: 'Constant', label: 'Constant' }]}
popupMatchSelectWidth={false}
variant="borderless"
onChange={() => handleInputTypeChange(caseIndex, conditionIndex)}
className="rb:w-18!"
/>
</Form.Item>
<Divider type="vertical" />
<Form.Item name={[conditionField.name, 'right']} noStyle>
{inputType === 'Variable'
?
<VariableSelect
placeholder={t('common.pleaseSelect')}
options={[{ value: 'Variable', label: 'Variable' }, { value: 'Constant', label: 'Constant' }]}
options={options.filter(vo => vo.dataType === 'number')}
allowClear={false}
popupMatchSelectWidth={false}
variant="borderless"
onChange={() => handleInputTypeChange(caseIndex, conditionIndex)}
size="small"
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name={[conditionField.name, 'right']} noStyle>
{inputType === 'Variable'
?
<VariableSelect
placeholder={t('common.pleaseSelect')}
options={options.filter(vo => vo.dataType === 'number')}
allowClear={false}
popupMatchSelectWidth={false}
: <InputNumber
placeholder={t('common.pleaseEnter')}
variant="borderless"
className="rb:w-full!"
onChange={(value) => form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'right'], value)}
/>
: <InputNumber
placeholder={t('common.pleaseEnter')}
variant="borderless"
className="rb:w-full!"
onChange={(value) => form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'right'], value)}
/>
}
</Form.Item>
</Col>
</Row>
}
</Form.Item>
</div>
: <Form.Item name={[conditionField.name, 'right']} noStyle>
{leftFieldType === 'boolean'
? <Radio.Group block>
<Radio.Button value={true}>True</Radio.Button>
<Radio.Button value={false}>False</Radio.Button>
</Radio.Group>
: <Editor options={options} />
: <Editor options={options} size="small" type="input" />
}
</Form.Item>
}
</>}
</div>}
</div>
<div
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
onClick={() => removeCondition(conditionField.name)}
></div>
</div>
)
})}
@@ -388,6 +385,8 @@ const CaseList: FC<CaseListProps> = ({
<Button
type="dashed"
block
size="middle"
className="rb:text-[12px]!"
onClick={() => handleAddCase(addCase)}
>
+ ELIF
@@ -395,9 +394,9 @@ const CaseList: FC<CaseListProps> = ({
</>
)}
</Form.List>
<Divider />
<div className="rb:font-medium">ELSE</div>
<div className="rb:text-[12px] rb:text-[#5B6167] ">{t('workflow.config.if-else.else_desc')}</div>
<div className="rb:font-medium rb:text-[12px] rb:mt-4 rb:leading-4.5">ELSE</div>
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-2 rb:leading-4.5">{t('workflow.config.if-else.else_desc')}</div>
</>
)
}

View File

@@ -1,8 +1,8 @@
import { type FC } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Form, Space } from 'antd';
import { DeleteOutlined } from '@ant-design/icons';
import { Graph, Node } from '@antv/x6';
import Editor from '../../Editor';
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
@@ -151,17 +151,15 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
const contentLength = (currentItem.class_name || '').length;
return (
<div key={key} className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:p-3 rb:bg-[#F8F9FB]">
<div key={key} className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:p-2 rb:bg-[#F8F9FB]">
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
<div>{t('workflow.config.question-classifier.class_name')} {index + 1}</div>
<div className="rb:text-[12px] rb:font-medium rb:py-1 rb:leading-2">{t('workflow.config.question-classifier.class_name')} {index + 1}</div>
<div className="rb:flex rb:items-center rb:gap-1">
<span className="rb:text-xs rb:text-gray-500">{contentLength}</span>
<Button
type="text"
size="small"
icon={<DeleteOutlined />}
<div
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
onClick={() => handleRemoveCategory(remove, name, index)}
/>
></div>
</div>
</div>
<Form.Item
@@ -172,6 +170,7 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
<Editor
placeholder={t('common.pleaseEnter')}
options={options}
size="small"
/>
</Form.Item>
</div>
@@ -179,8 +178,10 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
<Button
type="dashed"
size="middle"
block
onClick={() => handleAddCategory(add)}
className="rb:w-full"
className="rb:text-[12px]!"
>
+ {t('workflow.config.question-classifier.addClassName')}
</Button>

View File

@@ -1,11 +1,10 @@
import { type FC } from 'react'
import clsx from 'clsx'
import { useTranslation } from 'react-i18next';
import { Form, Button, Select, Row, Col, InputNumber, Radio, Input, type SelectProps } from 'antd'
import { DeleteOutlined } from '@ant-design/icons';
import { Form, Button, Select, InputNumber, Radio, Input, Divider, type SelectProps } from 'antd'
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
import VariableSelect from '../VariableSelect'
import Editor from '../../Editor'
interface Case {
logical_operator: 'and' | 'or';
@@ -84,52 +83,64 @@ const ConditionList: FC<CaseListProps> = ({
return (
<>
<Form.List name={[parentName, 'expressions']}>
{(fields, { add, remove }) => (
<div>
{(fields, { add, remove }) => {
const logicalOperator = form.getFieldValue([parentName, 'logical_operator']);
return (
<div className="rb:relative">
{fields.map((field, index) => {
const expressions = form.getFieldValue([parentName, 'expressions']) || [];
const currentExpression = expressions[index] || {};
const currentOperator = currentExpression.operator;
const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty';
const leftFieldValue = currentExpression.left;
const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue);
const leftFieldType = leftFieldOption?.dataType;
const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || [];
const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined;
const logicalOperator = form.getFieldValue([parentName, 'logical_operator']);
return (
<div key={field.key} className="rb:mb-3">
{index > 0 && (<>
<div className="rb:absolute rb:w-3 rb:left-2 rb:top-3.75 rb:bottom-3.75 rb:z-10 rb:border rb:border-[#DFE4ED] rb:rounded-l-md rb:border-r-0"></div>
<div className="rb:absolute rb:z-10 rb:left-0 rb:top-[50%] rb:transform-[translateY(-50%)]]">
<Form.Item name={[parentName, 'logical_operator']} noStyle >
<Button size="small" className="rb:cursor-pointer" onClick={handleChangeLogicalOperator}>{logicalOperator}</Button>
</Form.Item>
</div>
</>)}
<div className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:p-3 rb:bg-white rb:ml-6">
<Row gutter={8} align="middle">
<Col span={14}>
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
<div className="rb:text-[12px] rb:font-medium rb:leading-4.5">
{t('workflow.config.loop.condition')}
</div>
<Button
onClick={() => add({})}
className="rb:py-0! rb:px-1! rb:text-[12px]!"
size="small"
>
+ {t('workflow.config.loop.addCondition')}
</Button>
</div>
{fields?.length > 1 && <div className="rb:absolute rb:top-8 rb:bottom-4 rb:w-8.5 rb:h-[calc(100%-32px)]">
<div className="rb:absolute rb:w-2.5 rb:h-[calc(50%-30px)] rb:left-5 rb:top-4 rb:z-10 rb:border-l rb:border-t rb:border-[#DFE4ED] rb:rounded-tl-[10px] rb:border-r-0"></div>
<div className="rb:absolute rb:z-10 rb:left-0 rb:top-[calc(50%-13px)]">
<Form.Item name={[parentName, 'logical_operator']} noStyle >
<Button size="small" className="rb:text-[12px]! rb:py-px! rb:px-1! rb:w-8.5! rb:h-5!" onClick={handleChangeLogicalOperator}>{logicalOperator}</Button>
</Form.Item>
</div>
<div className="rb:absolute rb:w-2.5 rb:h-[calc(50%-30px)] rb:left-5 rb:bottom-4 rb:z-10 rb:border-l rb:border-b rb:border-[#DFE4ED] rb:rounded-bl-[10px] rb:border-r-0"></div>
</div>}
{fields.map((field, index) => {
const expressions = form.getFieldValue([parentName, 'expressions']) || [];
const currentExpression = expressions[index] || {};
const currentOperator = currentExpression.operator;
const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty';
const leftFieldValue = currentExpression.left;
const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue);
const leftFieldType = leftFieldOption?.dataType;
const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || [];
const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined;
return (
<div key={field.key} className="rb:flex rb:items-start rb:ml-9.5 rb:mb-4">
<div className="rb:flex-1 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-md">
<div className={clsx("rb:flex rb:gap-1 rb:p-1", {
'rb:border-b rb:border-b-[#DFE4ED]': !hideRightField
})}>
<Form.Item name={[field.name, 'left']} noStyle>
<VariableSelect
options={options.filter(vo =>
vo.value.includes('sys.') ||
vo.value.includes('conv.') ||
options={options.filter(vo =>
vo.value.includes('sys.') ||
vo.value.includes('conv.') ||
vo.nodeData.type === 'loop' ||
(vo.nodeData.cycle && vo.nodeData.cycle === selectedNode?.id)
)}
size="small"
allowClear={false}
popupMatchSelectWidth={false}
placeholder={t('common.pleaseSelect')}
onChange={(val) => handleLeftFieldChange(index, val)}
/>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name={[field.name, 'operator']} noStyle>
<Select
options={operatorList.map(vo => ({
@@ -138,84 +149,67 @@ const ConditionList: FC<CaseListProps> = ({
}))}
size="small"
popupMatchSelectWidth={false}
placeholder={t('common.pleaseSelect')}
/>
</Form.Item>
</Col>
<Col span={2}>
<DeleteOutlined
className="rb:text-gray-400 rb:cursor-pointer rb:hover:text-red-500"
onClick={() => remove(field.name)}
/>
</Col>
</div>
{!hideRightField && <>
{!hideRightField && <div className="rb:p-1">
{leftFieldType === 'number'
? <Col span={24}><Row>
<Col span={12}>
<Form.Item name={[field.name, 'input_type']} noStyle>
<Select
? <div className="rb:flex rb:items-center">
<Form.Item name={[field.name, 'input_type']} noStyle>
<Select
placeholder={t('common.pleaseSelect')}
options={[{ value: 'Variable', label: 'Variable' }, { value: 'Constant', label: 'Constant' }]}
popupMatchSelectWidth={false}
variant="borderless"
className="rb:w-full!"
onChange={() => handleInputTypeChange(index)}
/>
</Form.Item>
<Divider type="vertical" />
<Form.Item name={[field.name, 'right']} noStyle>
{inputType === 'Variable'
?
<VariableSelect
placeholder={t('common.pleaseSelect')}
options={[{ value: 'Variable', label: 'Variable' }, { value: 'Constant', label: 'Constant' }]}
options={options.filter(vo => vo.dataType === 'number')}
allowClear={false}
popupMatchSelectWidth={false}
variant="borderless"
className="rb:w-full!"
onChange={() => handleInputTypeChange(index)}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name={[field.name, 'right']} noStyle>
{inputType === 'Variable'
?
<VariableSelect
placeholder={t('common.pleaseSelect')}
options={options.filter(vo => vo.dataType === 'number')}
allowClear={false}
popupMatchSelectWidth={false}
variant="borderless"
className="rb:w-full!"
/>
: <InputNumber
placeholder={t('common.pleaseEnter')}
variant="borderless"
className="rb:w-full!"
onChange={(value) => form.setFieldValue([parentName, 'expressions', index, 'right'], value)}
/>
}
</Form.Item>
</Col>
</Row></Col>
: <Col span={24}>
<Form.Item name={[field.name, 'right']} noStyle>
{leftFieldType === 'boolean'
? <Radio.Group block>
<Radio.Button value={true}>True</Radio.Button>
<Radio.Button value={false}>False</Radio.Button>
</Radio.Group>
: <Input placeholder={t('common.pleaseEnter')} />
: <InputNumber
placeholder={t('common.pleaseEnter')}
variant="borderless"
className="rb:w-full!"
onChange={(value) => form.setFieldValue([parentName, 'expressions', index, 'right'], value)}
/>
}
</Form.Item>
</Col>
</div>
: <Form.Item name={[field.name, 'right']} noStyle>
{leftFieldType === 'boolean'
? <Radio.Group block>
<Radio.Button value={true}>True</Radio.Button>
<Radio.Button value={false}>False</Radio.Button>
</Radio.Group>
: <Input placeholder={t('common.pleaseEnter')} />
}
</Form.Item>
}
</>}
</Row>
</div>}
</div>
<div
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
onClick={() => remove(field.name)}
></div>
</div>
</div>
)
})}
)
})}
</div>
<Button
type="dashed"
onClick={() => add({ left: '', operator: '', right: '' })}
className="rb:w-full rb:ml-6 rb:mt-2"
icon={<span>+</span>}
>
</Button>
</div>
)}
)
}}
</Form.List>
</>
)

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'
import { Button, Select, Table, Form, type TableProps } from 'antd';
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
import { PlusOutlined } from '@ant-design/icons';
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin';
import Empty from '@/components/Empty';
import VariableSelect from '../VariableSelect';
@@ -19,6 +18,7 @@ interface EditableTableProps {
options?: Suggestion[];
typeOptions?: { value: string, label: string }[]
filterBooleanType?: boolean;
size?: "small"
}
const EditableTable: React.FC<EditableTableProps> = ({
@@ -26,7 +26,8 @@ const EditableTable: React.FC<EditableTableProps> = ({
title,
options = [],
typeOptions = [],
filterBooleanType = false
filterBooleanType = false,
size = 'small'
}) => {
const { t } = useTranslation();
@@ -38,21 +39,24 @@ const EditableTable: React.FC<EditableTableProps> = ({
const getColumns = (remove: (index: number) => void): TableProps<TableRow>['columns'] => {
const hasType = typeOptions.length > 0;
const baseWidth = hasType ? '35%' : '45%';
const cellClassName="rb:p-1!"
const contentClassName ="rb:w-[108px]! rb:text-[12px]!"
return [
{
title: t('workflow.config.name'),
dataIndex: 'name',
width: baseWidth,
className: cellClassName,
render: (_: any, __: TableRow, index: number) => (
<Form.Item name={[index, 'name']} noStyle>
<VariableSelect
placeholder={t('common.pleaseSelect')}
size="small"
// size="small"
options={options}
filterBooleanType={filterBooleanType}
popupMatchSelectWidth={false}
className={contentClassName}
size={size}
/>
</Form.Item>
)
@@ -61,18 +65,20 @@ const EditableTable: React.FC<EditableTableProps> = ({
title: t('workflow.config.type'),
dataIndex: 'type',
width: '20%',
className: cellClassName,
render: (_: any, __: TableRow, index: number) => (
<Form.Item shouldUpdate noStyle>
{(form) => (
<Form.Item name={[index, 'type']} noStyle>
<Select
placeholder={t('common.pleaseSelect')}
size="small"
// size="small"
options={typeOptions}
popupMatchSelectWidth={false}
onChange={() => {
form.setFieldValue([...Array.isArray(parentName) ? parentName : [parentName], index, 'value'], undefined);
}}
size={size}
/>
</Form.Item>
)}
@@ -82,7 +88,7 @@ const EditableTable: React.FC<EditableTableProps> = ({
{
title: t('workflow.config.value'),
dataIndex: 'value',
width: baseWidth,
className: cellClassName,
render: (_: any, __: TableRow, index: number) => (
<Form.Item
shouldUpdate={(prevValues, currentValues) => {
@@ -102,10 +108,12 @@ const EditableTable: React.FC<EditableTableProps> = ({
<Form.Item name={[index, 'value']} noStyle>
<VariableSelect
placeholder={t('common.pleaseSelect')}
size="small"
// size="small"
options={filteredOptions}
filterBooleanType={filterBooleanType}
popupMatchSelectWidth={false}
className={contentClassName}
size={size}
/>
</Form.Item>
);
@@ -116,9 +124,12 @@ const EditableTable: React.FC<EditableTableProps> = ({
{
title: '',
dataIndex: 'actions',
width: '10%',
className: cellClassName,
render: (_: any, __: TableRow, index: number) => (
<Button type="text" icon={<DeleteOutlined />} onClick={() => remove(index)} />
<div
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
onClick={() => remove(index)}
></div>
)
}
];
@@ -129,13 +140,11 @@ const EditableTable: React.FC<EditableTableProps> = ({
<Form.List name={parentName}>
{(fields, { add, remove }) => {
const AddButton = ({ block = false }: { block?: boolean }) => (
<Button
type={block ? "dashed" : "text"}
<Button
icon={block ? undefined : <PlusOutlined />}
onClick={() => add(createNewRow())}
size="small"
block={block}
className={block ? "rb:mt-1" : ""}
className={block ? "rb:mt-1 rb:text-[12px]! rb:bg-transparent!" : "rb:text-[12px]!"}
>
{block && `+${t('common.add')}`}
</Button>
@@ -145,8 +154,8 @@ const EditableTable: React.FC<EditableTableProps> = ({
<>
{title && (
<div className="rb:flex rb:items-center rb:mb-2 rb:justify-between">
<div className="rb:font-medium">{title}</div>
<AddButton />
<div className="rb:font-medium rb:text-[12px] rb:leading-4.5">{title}</div>
<AddButton block={true} />
</div>
)}
@@ -161,8 +170,9 @@ const EditableTable: React.FC<EditableTableProps> = ({
columns={getColumns(remove)}
pagination={false}
size="small"
rowClassName="rb:p-0! rb:bg-[#F6F8FC]!"
locale={{ emptyText: <Empty size={88} /> }}
scroll={{ x: 'max-content' }}
style={{ width: '274px' }}
/>
{!title && <AddButton block />}

View File

@@ -1,6 +1,7 @@
import { type FC, useRef } from "react";
import { type FC, useRef, useState } from "react";
import { useTranslation } from 'react-i18next'
import { Form, Row, Col, Select, Button, Divider, InputNumber, Switch, Input } from 'antd'
import { CaretDownOutlined, CaretRightOutlined, SettingOutlined } from '@ant-design/icons';
import Editor from '../../Editor'
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
import AuthConfigModal from './AuthConfigModal'
@@ -65,15 +66,23 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
}
}
console.log('HttpRequest', values)
const [collapsed, setCollapsed] = useState(true)
const handleToggle = () => {
setCollapsed((prev: boolean) => !prev)
}
return (
<>
<div className="rb:flex rb:items-center rb:justify-between rb:mb-4">
<div>API</div>
<Button onClick={handleChangeAuth}>{t('workflow.config.http-request.auth')}</Button>
<div className="rb:flex rb:items-center rb:justify-between rb:mb-1">
<div className="rb:font-medium rb:text-[12px] rb:leading-4.5">API</div>
<Button onClick={handleChangeAuth}
size="small"
type="text"
icon={<SettingOutlined />}
className="rb:mt-1 rb:text-[12px]!"
>{t('workflow.config.http-request.auth')}: {!values?.auth?.auth_type || values?.auth?.auth_type === 'none' ? t('workflow.config.http-request.none') : t('workflow.config.http-request.apiKey')}</Button>
</div>
<Row gutter={16}>
<Row gutter={4}>
<Col span={8}>
<Form.Item name="method">
<Select
@@ -85,35 +94,43 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
{ label: 'PUT', value: 'PUT' },
{ label: 'DELETE', value: 'DELETE' },
]}
className="rb:bg-transparent!"
/>
</Form.Item>
</Col>
<Col span={16}>
<Form.Item name="url">
<Editor options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')} variant="outlined" />
<Editor
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
variant="outlined"
type="input"
size="small"
/>
</Form.Item>
</Col>
</Row>
<Form.Item name="auth" hidden>
</Form.Item>
<Form.Item name="headers">
<Form.Item name="headers" noStyle>
<EditableTable
size="small"
parentName="headers"
title="HEADERS"
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
/>
</Form.Item>
<Form.Item name="params">
<Form.Item name="params" noStyle>
<EditableTable
size="small"
parentName="params"
title="PARAMS"
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
/>
</Form.Item>
<Form.Item label="BODY">
<Form.Item label="BODY" className="rb:mb-0!">
<Form.Item name={['body', 'content_type']}>
<Select
placeholder={t('common.pleaseSelect')}
@@ -131,6 +148,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
{values?.body?.content_type === 'form-data' &&
<Form.Item name={['body', 'data']} noStyle>
<EditableTable
size="small"
parentName={['body', 'data']}
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
typeOptions={[
@@ -143,6 +161,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
{values?.body?.content_type === 'x-www-form-urlencoded' &&
<Form.Item name={['body', 'data']} noStyle>
<EditableTable
size="small"
parentName={['body', 'data']}
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
filterBooleanType={true}
@@ -150,7 +169,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
</Form.Item>
}
{values?.body?.content_type === 'json' &&
<Form.Item name={['body', 'data']}>
<Form.Item name={['body', 'data']} noStyle>
<MessageEditor
key="json"
parentName={['body', 'data']}
@@ -161,7 +180,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
</Form.Item>
}
{values?.body?.content_type === 'raw' &&
<Form.Item name={['body', 'data']}>
<Form.Item name={['body', 'data']} noStyle>
<MessageEditor
key="raw"
parentName={['body', 'data']}
@@ -172,7 +191,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
</Form.Item>
}
{values?.body?.content_type === 'binary' &&
<Form.Item name={['body', 'data']}>
<Form.Item name={['body', 'data']} noStyle>
<VariableSelect
placeholder={t('common.pleaseSelect')}
options={options.filter(vo => vo.dataType.includes('file'))}
@@ -182,15 +201,19 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
}
</Form.Item>
<Divider />
<Form.Item layout="horizontal" name="verify_ssl" label={t('workflow.config.http-request.verify_ssl')}>
<Form.Item layout="horizontal" name="verify_ssl" label={t('workflow.config.http-request.verify_ssl')} className="rb:mb-0!">
<Switch />
</Form.Item>
<Divider />
<div>{t('workflow.config.http-request.timeouts')}</div>
<div className="rb:font-medium rb:text-[12px] rb:leading-4.5 rb:mb-2.5 rb:cursor-pointer" onClick={handleToggle}>
{t('workflow.config.http-request.timeouts')}
{collapsed ? <CaretRightOutlined /> : <CaretDownOutlined />}
</div>
<Form.Item
name={['timeouts', 'connect_timeout']}
label={t('workflow.config.http-request.connect_timeout')}
hidden={collapsed}
>
<InputNumber
placeholder={t('common.pleaseEnter')}
@@ -201,6 +224,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
<Form.Item
name={['timeouts', 'read_timeout']}
label={t('workflow.config.http-request.read_timeout')}
hidden={collapsed}
>
<InputNumber
placeholder={t('common.pleaseEnter')}
@@ -211,6 +235,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
<Form.Item
name={['timeouts', 'write_timeout']}
label={t('workflow.config.http-request.write_timeout')}
hidden={collapsed}
>
<InputNumber
placeholder={t('common.pleaseEnter')}

View File

@@ -1,6 +1,6 @@
import { type FC, useRef, useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { Space, Button, List } from 'antd'
import { Space, Button } from 'antd'
import knowledgeEmpty from '@/assets/images/application/knowledgeEmpty.svg'
import type {
KnowledgeConfigForm,
@@ -113,49 +113,65 @@ const Knowledge: FC<{value?: KnowledgeConfig; onChange?: (config: KnowledgeConfi
}
return (
<div>
<div className="rb:flex rb:justify-between rb:items-center">
<div>{t('application.knowledgeBaseAssociation')}</div>
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
<div className="rb:text-[12px] rb:font-medium rb:leading-4.5">
{t('application.knowledgeBaseAssociation')}
</div>
<Space>
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handleKnowledgeConfig}>{t('workflow.config.knowledge-retrieval.recallConfig')}</Button>
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handleAddKnowledge}>+</Button>
</Space>
<Button
onClick={handleKnowledgeConfig}
className="rb:py-0! rb:px-1! rb:text-[12px]! rb:group rb:gap-0.5!"
size="small"
>
<div
className="rb:size-3.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/recall.svg')] rb:group-hover:bg-[url('@/assets/images/workflow/recall_hover.svg')]"
></div>
{t('workflow.config.knowledge-retrieval.recallConfig')}
</Button>
</div>
{knowledgeList.length === 0
? <Empty url={knowledgeEmpty} size={88} subTitle={t('application.knowledgeEmpty')} />
:
<List
grid={{ gutter: 12, column: 1 }}
dataSource={knowledgeList}
renderItem={(item) => {
if (!item.id) return null
return (
<List.Item>
<div key={item.id} className="rb:flex rb:items-center rb:justify-between rb:p-[12px_16px] rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
<div className="rb:font-medium rb:leading-4">
{item.name}
<Tag color={item.status === 1 ? 'success' : item.status === 0 ? 'default' : 'error'} className="rb:ml-2">
{item.status === 1 ? t('common.enable') : item.status === 0 ? t('common.disabled') : t('common.deleted')}
</Tag>
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-5">{t('application.contains', {include_count: item.doc_num})}</div>
</div>
<Space size={12}>
<div
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
onClick={() => handleEditKnowledge(item)}
></div>
<div
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
onClick={() => handleDeleteKnowledge(item.id)}
></div>
</Space>
</div>
</List.Item>
)
}}
/>
}
<Space size={10} direction="vertical" className="rb:w-full!">
<Button
type="dashed"
block
size="middle"
className="rb:text-[12px]!"
onClick={handleAddKnowledge}
>
+ {t('workflow.config.knowledge-retrieval.addKnowledge')}
</Button>
{knowledgeList.length === 0
? <Empty url={knowledgeEmpty} size={88} subTitle={t('application.knowledgeEmpty')} />
: knowledgeList.map(item => {
if (!item.id) return null
return (
<div key={item.id} className="rb:text-[12px] rb:flex rb:items-center rb:justify-between rb:py-2 rb:px-2.5 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
<div className="">
<span className="rb:font-medium rb:leading-4">{item.name}</span>
<Tag
color={item.status === 1 ? 'success' : item.status === 0 ? 'default' : 'error'}
className="rb:ml-1 rb:py-0! rb:px-1! rb:text-[12px] rb:leading-3.5!"
>
{item.status === 1 ? t('common.enable') : item.status === 0 ? t('common.disabled') : t('common.deleted')}
</Tag>
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-5">{t('application.contains', { include_count: item.doc_num })}</div>
</div>
<Space size={12}>
<div
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
onClick={() => handleEditKnowledge(item)}
></div>
<div
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"
onClick={() => handleDeleteKnowledge(item.id)}
></div>
</Space>
</div>
)
})
}
</Space>
{/* 全局设置 */}
<KnowledgeGlobalConfigModal
data={editConfig}

View File

@@ -83,6 +83,7 @@ const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfig
<Form
form={form}
layout="vertical"
size="middle"
>
{data && (
<div className="rb:mb-6 rb:flex rb:items-center rb:justify-between rb:border rb:rounded-lg rb:p-[17px_16px] rb:cursor-pointer rb:bg-[#F0F3F8] rb:border-[#DFE4ED] rb:text-[#212332]">

View File

@@ -70,6 +70,7 @@ const KnowledgeGlobalConfigModal = forwardRef<KnowledgeGlobalConfigModalRef, Kno
<Form
form={form}
layout="vertical"
size="middle"
>
<div className="rb:text-[#5B6167] rb:mb-6">{t('application.globalConfigDesc')}</div>

View File

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

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { useTranslation } from 'react-i18next'
import { MinusCircleOutlined } from '@ant-design/icons';
import { Button, Form, Input, Space, Row, Col } from 'antd';
import { Button, Form, Input, Divider } from 'antd';
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
import VariableSelect from '../VariableSelect'
@@ -16,43 +15,55 @@ const MappingList: React.FC<MappingListProps> = ({ name, options }) => {
<Form.List name={name}>
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<Row key={key} gutter={12} className="rb:mb-2">
<Col span={10}>
<Form.Item
{...restField}
name={[name, 'name']}
noStyle
>
<Input placeholder={t('common.pleaseEnter')} data-field-type="mapping-name" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
{...restField}
name={[name, 'value']}
noStyle
>
<VariableSelect
placeholder={t('common.pleaseSelect')}
options={options}
popupMatchSelectWidth={false}
/>
</Form.Item>
</Col>
<Col span={2}>
<MinusCircleOutlined onClick={() => remove(name)} />
</Col>
</Row>
))}
<Form.Item>
<Button type="dashed" onClick={() => add()} block>
+ {t('common.add')}
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
<div className="rb:text-[12px] rb:font-medium rb:leading-4.5">
{t('workflow.config.jinja-render.mapping')}
</div>
<Button
onClick={() => add()}
className="rb:py-0! rb:px-1! rb:text-[12px]!"
size="small"
>
+ {t('workflow.config.addVariable')}
</Button>
</Form.Item>
</div>
{fields.map(({ key, name, ...restField }) => (
<div key={key} className="rb:flex rb:items-center rb:gap-1 rb:mb-2">
<Form.Item
{...restField}
name={[name, 'name']}
noStyle
>
<Input
placeholder={t('common.pleaseEnter')}
size="small"
className="rb:w-24!"
/>
</Form.Item>
<Form.Item
{...restField}
name={[name, 'value']}
noStyle
>
<VariableSelect
placeholder={t('common.pleaseSelect')}
options={options}
popupMatchSelectWidth={false}
size="small"
className="rb:w-39!"
/>
</Form.Item>
<div
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
onClick={() => remove(name)}
></div>
</div>
))}
</>
)}
</Form.List>
<Divider />
</>
)
};

View File

@@ -30,7 +30,7 @@ const MemoryConfig: FC<{ options: Suggestion[]; parentName: string; }> = ({
return (
<>
{values?.memory?.enable && <>
<div className="rb:flex rb:items-center rb:justify-between rb:py-1.5 rb:px-2 rb:bg-[#F6F8FC] rb:rounded-md rb:mb-2">
<div className="rb:flex rb:items-center rb:justify-between rb:py-1.5 rb:px-2 rb:text-[12px] rb:bg-[#F6F8FC] rb:rounded-md rb:mb-2">
{t('workflow.config.llm.memory')}
<span>{t('workflow.config.llm.inner')}</span>
</div>
@@ -40,6 +40,7 @@ const MemoryConfig: FC<{ options: Suggestion[]; parentName: string; }> = ({
isArray={false}
parentName={[parentName, 'messages']}
options={options}
size="small"
/>
</Form.Item>

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import { type FC, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { Button, Space, List } from 'antd'
import Empty from '@/components/Empty'
import { Button, Space } from 'antd'
import type { ParamItem, ParamEditModalRef } from './types'
import ParamEditModal from './ParamEditModal'
@@ -41,47 +41,37 @@ const ParamsList: FC<ParamsListProps> = ({
}
return (
<div>
<div className="rb:flex rb:justify-between rb:items-center">
<div>{label}</div>
<Space>
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handleAdd}>+</Button>
</Space>
<div className="rb:leading-4.25 rb:text-[12px] rb:font-medium rb:mb-2">
{label}
</div>
{value?.length === 0
? <Empty size={88} />
:
<List
grid={{ gutter: 12, column: 1 }}
dataSource={value}
renderItem={(item, index) => (
<List.Item>
<div key={index} className="rb:group rb:relative rb:p-[12px_16px] rb:bg-[#FBFDFF] rb:cursor-pointer rb:border rb:border-[#DFE4ED] rb:rounded-lg">
<div className="rb:flex rb:items-center rb:justify-between">
<div className="rb:leading-4">
<span className="rb:font-medium">{item.name}</span>
<span className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular"> ({t(`workflow.config.parameter-extractor.${item.type}`)})</span>
</div>
<span className="rb:block rb:group-hover:hidden rb:text-[12px] rb:text-[#5B6167] rb:font-regular">{item.required ? t('workflow.config.parameter-extractor.required') : ''}</span>
<Space size={10} direction="vertical" className="rb:w-full!">
<Button type="dashed" block size="middle" className="rb:text-[12px]!" onClick={handleAdd}>+ {t('workflow.config.parameter-extractor.addParams')}</Button>
</div>
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:wrap-break-word rb:line-clamp-1">{item.desc}</div>
<Space size={12} className="rb:hidden! rb:group-hover:flex! rb:absolute rb:right-4 rb:top-[50%] rb:transform-[translateY(-50%)] rb:bg-white">
<div
className="rb:size-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
onClick={() => handleEdit(index)}
></div>
<div
className="rb:size-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
onClick={() => handleDelete(index)}
></div>
</Space>
</div>
</List.Item>
)}
/>
}
{value?.map((item, index) => (
<div
key={index}
className="rb:cursor-pointer rb:group rb:py-2 rb:pl-2.5 rb:pr-2 rb:text-[12px] rb:flex rb:items-center rb:justify-between rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-md"
>
<div>
<span className="rb:font-medium">{item.name}</span>
<span className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular"> ({t(`workflow.config.parameter-extractor.${item.type}`)}) {item.required ? t('workflow.config.parameter-extractor.required') : ''}</span>
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4.25 rb:mt-0.5">{item.desc}</div>
</div>
<Space size={8}>
<div
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
onClick={() => handleEdit(index)}
></div>
<div
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"
onClick={() => handleDelete(index)}
></div>
</Space>
</div>
))}
</Space>
<ParamEditModal
ref={paramEditModalRef}

View File

@@ -0,0 +1,55 @@
import React, { useState, useEffect } from 'react';
import type { InputNumberProps, SliderSingleProps } from 'antd';
import { Col, InputNumber, Row, Slider } from 'antd';
const RbSlider: React.FC<SliderSingleProps> = ({
value,
onChange,
min,
max,
step = 0.01,
...props
}) => {
const [curValue, setCurValue] = useState<number | undefined>(0)
useEffect(() => {
setCurValue(value)
}, [value])
const handleSliderChange = (newValue: number) => {
onChange && onChange(newValue);
};
const handleInputChange: InputNumberProps['onChange'] = (newValue) => {
onChange && onChange(newValue as number);
};
return (
<Row gutter={12}>
<Col span={16}>
<Slider
{...props}
min={min}
max={max}
step={step as number}
value={curValue}
className="rb:my-0! rb:ml-2.5!"
classNames={{
rail: 'rb:h-[6px]!',
track: 'rb:h-[6px]!'
}}
onChange={handleSliderChange}
/>
</Col>
<Col span={8}>
<InputNumber
min={min}
max={max}
step={step as number}
value={curValue}
onChange={handleInputChange}
className="rb:w-full!"
/>
</Col>
</Row>
);
};
export default RbSlider;

View File

@@ -171,6 +171,7 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({
label={t('workflow.config.tool.tool_id')}
>
<Cascader
placeholder={t('common.pleaseSelect')}
options={optionList}
loadData={loadData}
onChange={handleChange}
@@ -187,28 +188,30 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({
label={parameter.name}
extra={parameter.type === 'boolean' ? undefined : parameter.description}
rules={[
{ required: parameter.required, message: t('workflow.config.tool.required') }
{ required: parameter.required, message: t('common.pleaseEnter') }
]}
layout={parameter.type === 'boolean' ? 'horizontal' : 'vertical'}
className={parameter.type === 'boolean' ? 'rb:mb-0!' : ''}
>
{parameter.type === 'string' && parameter.enum && parameter.enum.length > 0
? <Select options={parameter.enum.map(vo => ({ value: vo, label: vo }))} placeholder={t('common.pleaseSelect')} />
? <Select size="small" options={parameter.enum.map(vo => ({ value: vo, label: vo }))} placeholder={t('common.pleaseSelect')} />
: parameter.type === 'boolean'
? <Switch />
? <Switch size="small" />
: parameter.type === 'integer' || parameter.type === 'number'
? <InputNumber
min={parameter.minimum}
max={parameter.maximum}
step={parameter.type === 'integer' ? 1 : 0.01}
placeholder={t('common.pleaseEnter')}
className="rb:w-full!"
className="rb:w-full!"
size="small"
onChange={(value) => form.setFieldValue(['tool_parameters', parameter.name], value)}
/>
: <Editor
height={32}
variant="outlined"
options={options}
size="small"
placeholder={t('common.pleaseEnter')}
/>
}

View File

@@ -2,14 +2,14 @@ import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, Select, InputNumber, Checkbox, Tag } from 'antd';
import { useTranslation } from 'react-i18next';
import type { StartVariableItem, VariableEditModalRef } from '../../types'
import type { Variable, VariableEditModalRef } from './types'
import RbModal from '@/components/RbModal'
import SortableList from '@/components/SortableList'
const FormItem = Form.Item;
interface VariableEditModalProps {
refresh: (values: StartVariableItem) => void;
refresh: (values: Variable) => void;
}
const types = [
@@ -36,9 +36,9 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
}, ref) => {
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
const [form] = Form.useForm<StartVariableItem>();
const [form] = Form.useForm<Variable>();
const [loading, setLoading] = useState(false)
const [editVo, setEditVo] = useState<StartVariableItem | null>(null)
const [editVo, setEditVo] = useState<Variable | null>(null)
const values = Form.useWatch([], form);
@@ -50,7 +50,7 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
setEditVo(null)
};
const handleOpen = (variable?: StartVariableItem) => {
const handleOpen = (variable?: Variable) => {
setVisible(true);
if (variable) {
setEditVo(variable || null)
@@ -85,7 +85,7 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
return (
<RbModal
title={editVo ? t('workflow.config.start.editVariable') : t('workflow.config.start.addVariable')}
title={editVo ? t('workflow.config.start.editVariable') : t('workflow.config.addVariable')}
open={visible}
onCancel={handleClose}
okText={t('common.save')}
@@ -96,6 +96,7 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
form={form}
layout="vertical"
initialValues={initialValues}
size="middle"
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }}
>
{/* 变量类型 */}

View File

@@ -0,0 +1,108 @@
import { type FC, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Node } from '@antv/x6';
import { Space, Button, Divider, App } from 'antd'
import type { Variable, VariableEditModalRef } from './types'
import type { NodeConfig } from '../../../types'
import VariableEditModal from './VariableEditModal'
interface VariableListProps {
selectedNode?: Node | null;
config: NodeConfig;
value?: Variable[];
parentName: string;
onChange?: (value: Variable[]) => void;
}
const VariableList: FC<VariableListProps> = ({
value = [],
onChange,
selectedNode,
config,
parentName
}) => {
const { t } = useTranslation()
const { modal } = App.useApp()
const variableModalRef = useRef<VariableEditModalRef>(null)
const [editIndex, setEditIndex] = useState<number | null>(null)
const handleAddVariable = () => {
setEditIndex(null)
variableModalRef.current?.handleOpen()
}
const handleEditVariable = (index: number, vo: Variable) => {
variableModalRef.current?.handleOpen(vo)
setEditIndex(index)
}
const handleRefreshVariable = (variable: Variable) => {
if (!selectedNode) return
if (editIndex !== null) {
const list = [...value]
list[editIndex] = variable
onChange?.(list)
} else {
console.log('VariableList', value, variable)
onChange?.([...value, variable])
}
}
const handleDeleteVariable = (index: number, vo: Variable, e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (!selectedNode) return
modal.confirm({
title: t('common.confirmDeleteDesc', { name: vo.name }),
okText: t('common.delete'),
cancelText: t('common.cancel'),
okType: 'danger',
onOk: () => {
const list = [...value]
list.splice(index, 1)
onChange?.([...list])
}
})
}
return (
<div>
<Space size={10} direction="vertical" className="rb:w-full">
<div className="rb:leading-4.25 rb:text-[12px] rb:font-medium">
{t(`workflow.config.${selectedNode?.data?.type}.${parentName}`)}
</div>
<Button type="dashed" block size="middle" className="rb:text-[12px]!" onClick={handleAddVariable}>+ {t('workflow.config.addVariable')}</Button>
{Array.isArray(value) && value?.map((vo, index) =>
<div
key={`${vo.name}}-${index}`}
className="rb:cursor-pointer rb:group rb:py-2 rb:pl-2.5 rb:pr-2 rb:text-[12px] rb:flex rb:items-center rb:justify-between rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-md"
onClick={() => handleEditVariable(index, vo)}
>
<span className="rb:font-medium">{vo.name}·{vo.description}</span>
<Space size={8}>
{vo.required && <span className="rb:py-px rb:px-2 rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-sm">{t('workflow.config.start.required')}</span>}
<span className="rb:py-px rb:px-2 rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-sm">{vo.type}</span>
<div
className="rb:size-3 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/close.svg')] rb:hover:bg-[url('@/assets/images/close_hover.svg')]"
onClick={(e) => handleDeleteVariable(index, vo, e)}
></div>
</Space>
</div>
)}
</Space>
<Divider size="small" />
<Space size={10} direction="vertical" className="rb:w-full">
{config.sys?.map((vo, index) =>
<div key={index} className="rb:py-2 rb:pl-2.5 rb:pr-2 rb:text-[12px] rb:flex rb:items-center rb:justify-between rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-md">
<span className="rb:font-medium">sys.{vo.name}</span>
<span className="rb:py-px rb:px-2 rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-sm">{vo.type}</span>
</div>
)}
</Space>
<VariableEditModal
ref={variableModalRef}
refresh={handleRefreshVariable}
/>
</div>
)
}
export default VariableList

View File

@@ -0,0 +1,23 @@
export interface Variable {
name: string;
type: string;
required: boolean;
description: string;
max_length?: number;
default?: string;
readonly?: boolean;
defaultValue?: any;
value?: any;
}
export interface VariableEditModalRef {
handleOpen: (values?: Variable) => void;
}
export interface ApiExtensionModalData {
name: string;
apiEndpoint: string;
apiKey: string;
}
export interface ApiExtensionModalRef {
handleOpen: () => void;
}

View File

@@ -10,6 +10,7 @@ interface VariableSelectProps extends SelectProps {
onChange?: (value: string) => void;
allowClear?: boolean;
filterBooleanType?: boolean;
size?: 'small' | 'middle' | 'large'
}
const VariableSelect: FC<VariableSelectProps> = ({
@@ -18,7 +19,7 @@ const VariableSelect: FC<VariableSelectProps> = ({
value,
allowClear = true,
onChange,
size,
size = 'middle',
filterBooleanType = false,
...resetPorps
}) => {

View File

@@ -1,13 +1,13 @@
import { type FC, useEffect, useState, useRef, useMemo } from "react";
import clsx from 'clsx'
import { useTranslation } from 'react-i18next'
import { Graph, Node } from '@antv/x6';
import { Form, Input, Button, Select, InputNumber, Slider, Space, Divider, App, Switch } from 'antd'
import { Form, Input, Select, InputNumber, Switch } from 'antd'
import type { NodeConfig, NodeProperties, StartVariableItem, VariableEditModalRef, ChatVariable } from '../../types'
import type { NodeConfig, NodeProperties, ChatVariable } from '../../types'
import Empty from '@/components/Empty';
import emptyIcon from '@/assets/images/workflow/empty.png'
import CustomSelect from "@/components/CustomSelect";
import VariableEditModal from './VariableEditModal';
import MessageEditor from './MessageEditor'
import Knowledge from './Knowledge/Knowledge';
import type { Suggestion } from '../Editor/plugin/AutocompletePlugin'
@@ -23,7 +23,11 @@ import CycleVarsList from './CycleVarsList'
import AssignmentList from './AssignmentList'
import ToolConfig from './ToolConfig'
import MemoryConfig from './MemoryConfig'
import VariableList from './VariableList'
// import { calculateVariableList } from './utils/variableListCalculator'
import styles from './properties.module.css'
import Editor from "../Editor";
import RbSlider from './RbSlider'
interface PropertiesProps {
selectedNode?: Node | null;
@@ -42,12 +46,9 @@ const Properties: FC<PropertiesProps> = ({
chatVariables
}) => {
const { t } = useTranslation()
const { modal } = App.useApp()
const [form] = Form.useForm<NodeConfig>();
const [configs, setConfigs] = useState<Record<string,NodeConfig>>({} as Record<string,NodeConfig>)
const values = Form.useWatch([], form);
const variableModalRef = useRef<VariableEditModalRef>(null)
const [editIndex, setEditIndex] = useState<number | null>(null)
const [graphUpdateTrigger, setGraphUpdateTrigger] = useState(0)
const prevMappingNamesRef = useRef<string[]>([])
const prevTemplateVarsRef = useRef<string[]>([])
@@ -243,49 +244,6 @@ const Properties: FC<PropertiesProps> = ({
}
}, [values, selectedNode, form])
const handleAddVariable = () => {
setEditIndex(null)
variableModalRef.current?.handleOpen()
}
const handleEditVariable = (index: number, vo: StartVariableItem) => {
variableModalRef.current?.handleOpen(vo)
setEditIndex(index)
}
const handleRefreshVariable = (value: StartVariableItem) => {
if (!selectedNode) return
if (editIndex !== null) {
const defaultValue = selectedNode.data.config.variables.defaultValue ?? []
defaultValue[editIndex] = value
selectedNode.data.config.variables.defaultValue = [...defaultValue]
} else {
const defaultValue = selectedNode.data.config.variables.defaultValue ?? []
selectedNode.data.config.variables.defaultValue = [...defaultValue, value]
}
selectedNode?.setData({ ...selectedNode.data})
setConfigs({ ...selectedNode.data.config })
}
const handleDeleteVariable = (index: number, vo: StartVariableItem) => {
if (!selectedNode) return
modal.confirm({
title: t('common.confirmDeleteDesc', { name: vo.name }),
okText: t('common.delete'),
cancelText: t('common.cancel'),
okType: 'danger',
onOk: () => {
const defaultValue = selectedNode.data.config.variables.defaultValue ?? []
defaultValue.splice(index, 1)
selectedNode.data.config.variables.defaultValue = [...defaultValue]
selectedNode?.setData({ ...selectedNode.data })
setConfigs({ ...selectedNode.data.config })
}
})
}
const variableList = useMemo(() => {
if (!selectedNode || !graphRef?.current) return [];
@@ -586,7 +544,7 @@ const Properties: FC<PropertiesProps> = ({
break
case 'question-classifier':
const classNameKey = `${dataNodeId}_class_name`;
const outputKey = `${dataNodeId}_output`;
// const outputKey = `${dataNodeId}_output`;
if (!addedKeys.has(classNameKey)) {
addedKeys.add(classNameKey);
variableList.push({
@@ -1039,11 +997,11 @@ const Properties: FC<PropertiesProps> = ({
console.log('variableList', variableList)
return (
<div className="rb:w-75 rb:fixed rb:right-0 rb:top-16 rb:bottom-0 rb:p-3">
<div className="rb:font-medium rb:leading-5 rb:mb-3">{t('workflow.nodeProperties')}</div>
<div className={clsx("rb:w-75 rb:fixed rb:right-0 rb:top-16 rb:bottom-0 rb:p-3 rb:pb-6", styles.properties)}>
<div className="rb:font-medium rb:leading-5 rb:pb-3 rb:mb-3 rb:border-b rb:border-b-[#DFE4ED]">{t('workflow.nodeProperties')}</div>
{!selectedNode
? <Empty url={emptyIcon} size={140} className="rb:h-full rb:mx-15" title={t('workflow.empty')} />
: <Form form={form} layout="vertical" className="rb:h-[calc(100%-20px)] rb:overflow-y-auto">
: <Form form={form} size="small" layout="vertical" className="rb:h-[calc(100%-20px)] rb:overflow-x-hidden rb:overflow-y-auto">
<Form.Item name="name" label={t('workflow.nodeName')}>
<Input
placeholder={t('common.pleaseEnter')}
@@ -1073,46 +1031,13 @@ const Properties: FC<PropertiesProps> = ({
if (selectedNode?.data?.type === 'start' && key === 'variables' && config.type === 'define') {
return (
<div key={key}>
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2.75">
<div className="rb:leading-5">
{t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
</div>
<Button style={{padding: '0 8px', height: '24px'}} onClick={handleAddVariable}>+{t('application.addVariables')}</Button>
</div>
<Space size={4} direction="vertical" className="rb:w-full">
{Array.isArray(config.defaultValue) && config.defaultValue?.map((vo, index) =>
<div key={`${vo.name}}-${index}`} className="rb:p-[4px_8px] rb:text-[12px] rb:text-[#5B6167] rb:flex rb:items-center rb:justify-between rb:border rb:border-[#DFE4ED] rb:rounded-md rb:group rb:cursor-pointer">
<span>{vo.name}·{vo.description}</span>
<div className="rb:group-hover:hidden rb:flex rb:items-center rb:gap-1">
{vo.required && <span>{t('workflow.config.start.required')}</span>}
{vo.type}
</div>
<Space className="rb:hidden! rb:group-hover:flex!">
<div
className="rb:w-4.5 rb:h-4.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
onClick={() => handleEditVariable(index, vo)}
></div>
<div
className="rb:w-4.5 rb:h-4.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
onClick={() => handleDeleteVariable(index, vo)}
></div>
</Space>
</div>
)}
<Divider size="small" />
{config.sys?.map((vo, index) =>
<div key={index} className="rb:p-[4px_8px] rb:text-[12px] rb:text-[#5B6167] rb:flex rb:items-center rb:justify-between rb:border rb:border-[#DFE4ED] rb:rounded-md">
<div>
<span>sys.{vo.name}</span>
</div>
{vo.type}
</div>
)}
</Space>
</div>
<Form.Item key={key} name={key}>
<VariableList
parentName={key}
selectedNode={selectedNode}
config={config}
/>
</Form.Item>
)
}
@@ -1143,23 +1068,12 @@ const Properties: FC<PropertiesProps> = ({
key={key}
options={contextVariableList.filter(variable => variable.nodeData?.type !== 'knowledge-retrieval')}
parentName={key}
placeholder={t(config.placeholder || 'common.pleaseSelect')}
size="small"
/>
</Form.Item>
)
}
if (selectedNode?.data?.type === 'end' && key === 'output') {
return (
<Form.Item key={key} name={key}>
<MessageEditor
key={key}
isArray={false}
parentName={key}
options={variableList.filter(variable => variable.nodeData?.type !== 'knowledge-retrieval')}
/>
</Form.Item>
)
}
if (config.type === 'define') {
return null
}
@@ -1184,6 +1098,8 @@ const Properties: FC<PropertiesProps> = ({
parentName={key}
enableJinja2={config.enableJinja2 as boolean}
options={getFilteredVariableList(selectedNode?.data?.type, key)}
titleVariant={config.titleVariant}
size="small"
/>
</Form.Item>
)
@@ -1206,9 +1122,9 @@ const Properties: FC<PropertiesProps> = ({
name={key}
options={getFilteredVariableList(selectedNode?.data?.type, key)}
isCanAdd={!!(values as any)?.group}
size="small"
/>
</Form.Item>
)
}
if (config.type === 'caseList') {
@@ -1226,9 +1142,7 @@ const Properties: FC<PropertiesProps> = ({
if (config.type === 'mappingList') {
return (
<Form.Item key={key} name={key}
label={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
>
<Form.Item key={key} name={key} noStyle>
<MappingList name={key} options={getFilteredVariableList(selectedNode?.data?.type, key)} />
</Form.Item>
@@ -1238,6 +1152,7 @@ const Properties: FC<PropertiesProps> = ({
return (
<Form.Item key={key} name={key}>
<CycleVarsList
size="small"
parentName={key}
options={getFilteredVariableList(selectedNode?.data?.type, key)}
/>
@@ -1276,87 +1191,14 @@ const Properties: FC<PropertiesProps> = ({
</Form.Item>
)
}
return (
<Form.Item
key={key}
name={key}
label={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
layout={config.type === 'switch' ? 'horizontal' : 'vertical'}
>
{config.type === 'input'
? <Input placeholder={t('common.pleaseEnter')} />
: config.type === 'textarea'
? <Input.TextArea placeholder={t('common.pleaseEnter')} />
: config.type === 'select'
? <Select
options={config.needTranslation ? (config.options || []).map(vo => ({ ...vo, label: t(vo.label) })) : config.options}
placeholder={t('common.pleaseSelect')}
/>
: config.type === 'inputNumber'
? <InputNumber
placeholder={t('common.pleaseEnter')}
className="rb:w-full!"
onChange={(value) => form.setFieldValue(key, value)}
/>
: config.type === 'slider'
? <Slider min={config.min} max={config.max} step={config.step} />
: config.type === 'customSelect'
? <CustomSelect
placeholder={t('common.pleaseSelect')}
url={config.url as string}
params={config.params}
hasAll={false}
valueKey={config.valueKey}
labelKey={config.labelKey}
/>
: config.type === 'variableList'
? <VariableSelect
placeholder={t('common.pleaseSelect')}
options={(() => {
const baseVariableList = getFilteredVariableList(selectedNode?.data?.type, key);
// Apply filtering if specified in config
if (config.filterNodeTypes || config.filterVariableNames) {
return baseVariableList.filter(variable => {
const nodeTypeMatch = !config.filterNodeTypes ||
(Array.isArray(config.filterNodeTypes) && config.filterNodeTypes.includes(variable.nodeData?.type));
const variableNameMatch = !config.filterVariableNames ||
(Array.isArray(config.filterVariableNames) && config.filterVariableNames.includes(variable.label));
return nodeTypeMatch || variableNameMatch;
});
}
// Filter child nodes for iteration output
if (config.filterChildNodes && selectedNode) {
const graph = graphRef.current;
if (!graph) return [];
const nodes = graph.getNodes();
// Find child nodes whose cycle field equals parent node's ID
const childNodes = nodes.filter(node => {
const nodeData = node.getData();
return nodeData?.cycle === selectedNode.id;
});
return baseVariableList.filter(variable =>
childNodes.some(node => node.id === variable.nodeData?.id) || selectedNode?.data?.type === 'iteration' && key === 'output' && variable.value.includes('sys.')
);
}
return baseVariableList;
})()
}
/>
: config.type === 'switch'
? <Switch onChange={key === 'group' ? () => { form.setFieldValue('group_variables', []) } : undefined} />
: config.type === 'categoryList'
? <CategoryList
parentName={key}
selectedNode={selectedNode}
graphRef={graphRef}
options={getFilteredVariableList(selectedNode?.data?.type, key)}
/>
: config.type === 'conditionList'
? <ConditionList
if (config.type === 'conditionList') {
return (
<Form.Item
key={key}
name={key}
noStyle
>
<ConditionList
parentName={key}
options={(() => {
const cycleVars = values?.cycle_vars || [];
@@ -1375,6 +1217,93 @@ const Properties: FC<PropertiesProps> = ({
graphRef={graphRef}
addBtnText={t('workflow.config.addCase')}
/>
</Form.Item>
)
}
return (
<Form.Item
key={key}
name={key}
label={key === 'parallel_count' ? <span className="rb:text-[10px] rb:text-[#5B6167] rb:leading-3.5 rb:-mb-1!">{t(`workflow.config.${selectedNode?.data?.type}.${key}`)}</span> : t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
layout={config.type === 'switch' ? 'horizontal' : 'vertical'}
className={key === 'parallel_count' ? 'rb:-mt-3! rb:leading-3.5!' : ''}
>
{config.type === 'input'
? <Input placeholder={t('common.pleaseEnter')} />
: config.type === 'textarea'
? <Input.TextArea placeholder={t('common.pleaseEnter')} />
: config.type === 'select'
? <Select
options={config.needTranslation ? (config.options || []).map(vo => ({ ...vo, label: t(vo.label) })) : config.options}
placeholder={t('common.pleaseSelect')}
/>
: config.type === 'inputNumber'
? <InputNumber
placeholder={t('common.pleaseEnter')}
className="rb:w-full!"
onChange={(value) => form.setFieldValue(key, value)}
/>
: config.type === 'slider'
? <RbSlider min={config.min} max={config.max} step={config.step} />
: config.type === 'customSelect'
? <CustomSelect
placeholder={t('common.pleaseSelect')}
url={config.url as string}
params={config.params}
hasAll={false}
valueKey={config.valueKey}
labelKey={config.labelKey}
size="small"
/>
: config.type === 'variableList'
? <VariableSelect
placeholder={t(config.placeholder || 'common.pleaseSelect')}
options={(() => {
const baseVariableList = getFilteredVariableList(selectedNode?.data?.type, key);
// Apply filtering if specified in config
if (config.filterNodeTypes || config.filterVariableNames) {
return baseVariableList.filter(variable => {
const nodeTypeMatch = !config.filterNodeTypes ||
(Array.isArray(config.filterNodeTypes) && config.filterNodeTypes.includes(variable.nodeData?.type));
const variableNameMatch = !config.filterVariableNames ||
(Array.isArray(config.filterVariableNames) && config.filterVariableNames.includes(variable.label));
return nodeTypeMatch || variableNameMatch;
});
}
// Filter child nodes for iteration output
if (config.filterChildNodes && selectedNode) {
const graph = graphRef.current;
if (!graph) return [];
const nodes = graph.getNodes();
// Find child nodes whose cycle field equals parent node's ID
const childNodes = nodes.filter(node => {
const nodeData = node.getData();
return nodeData?.cycle === selectedNode.id;
});
return baseVariableList.filter(variable =>
childNodes.some(node => node.id === variable.nodeData?.id) || selectedNode?.data?.type === 'iteration' && key === 'output' && variable.value.includes('sys.')
);
}
return baseVariableList;
})()
}
size="small"
/>
: config.type === 'switch'
? <Switch onChange={key === 'group' ? () => { form.setFieldValue('group_variables', []) } : undefined} />
: config.type === 'categoryList'
? <CategoryList
parentName={key}
selectedNode={selectedNode}
graphRef={graphRef}
options={getFilteredVariableList(selectedNode?.data?.type, key)}
/>
: config.type === 'editor'
? <Editor options={variableList} variant="outlined" size="small" />
: null
}
</Form.Item>
@@ -1383,11 +1312,6 @@ const Properties: FC<PropertiesProps> = ({
}
</Form>
}
<VariableEditModal
ref={variableModalRef}
refresh={handleRefreshVariable}
/>
</div>
);
};

View File

@@ -0,0 +1,90 @@
.properties :global(.ant-input-outlined),
.properties :global(.ant-input-number-outlined),
.properties :global(.ant-select-outlined:not(.ant-select-customize-input) .ant-select-selector),
.properties :global(.ant-input-number-outlined),
.properties :global(.ant-input-number-outlined .ant-input-number-handler-wrap) {
background-color: transparent;
}
.properties :global(.ant-input-outlined.ant-input-disabled),
.properties :global(.ant-input-outlined[disabled]) {
background-color: #F6F8FC;
}
.properties :global(.ant-select-single.ant-select-sm){
height: 28px;
}
.properties :global(.ant-table-wrapper .ant-table-thead>tr>th),
.properties :global(.ant-table-wrapper .ant-table-thead>tr>td),
.properties :global(.ant-table-wrapper .ant-table) {
background-color: #F6F8FC;
}
.properties :global(.ant-table-wrapper .ant-table),
.properties :global(.ant-table-container),
.properties :global(.ant-table-wrapper table) {
border-radius: 6px;
}
.properties :global(.ant-table-wrapper .ant-table-container table>thead>tr:first-child>*:first-child) {
border-start-start-radius: 6px;
}
.properties :global(.ant-table-wrapper .ant-table-container table>thead>tr:first-child>*:last-child) {
border-start-end-radius: 6px;
}
.properties :global(.ant-table-row:last-child .ant-table-cell:first-child) {
border-bottom-left-radius: 6px;
}
.properties :global(.ant-table-row:last-child .ant-table-cell:last-child) {
border-bottom-right-radius: 6px;
}
.properties :global(.ant-table-wrapper .ant-table) {
background: transparent;
}
.properties :global(.ant-table-wrapper .ant-table-container) {
border-start-start-radius: 6px;
border-start-end-radius: 6px;
}
.properties :global(.ant-table-container) {
/* border-left: none;
border-top: none;
border-bottom: none; */
border: none;
}
.properties :global(.ant-table-wrapper .ant-table-tbody>tr.ant-table-placeholder:hover>th),
.properties :global(.ant-table-wrapper .ant-table-tbody>tr.ant-table-placeholder:hover>td),
.properties :global(.ant-table-wrapper .ant-table-tbody>tr.ant-table-placeholder),
.properties :global(.ant-table-wrapper .ant-table) {
background-color: #F6F8FC;
}
.properties :global(.ant-form-item-horizontal.ant-form-item .ant-form-item-control-input-content:has(> .ant-switch:only-child, > .ant-rate:only-child)) {
display: flex;
justify-content: end;
}
.properties :global(.ant-divider-horizontal.ant-divider-sm) {
margin-block: 16px;
}
.properties :global(.ant-form-item) {
margin-bottom: 16px;
}
.properties :global(.ant-form-item .ant-form-item-label>label) {
font-weight: 500;
font-size: 12px;
}
.properties :global(.ant-select-single.ant-select-sm .ant-select-selector),
.properties :global(.ant-select-dropdown .ant-select-item),.properties :global(.ant-input-number-sm) {
font-size: 12px;
}
.properties :global(.ant-input-number-out-of-range .ant-input-number-input-wrap input) {
color: #212332;
}
.properties :global(.ant-slider-horizontal .ant-slider-step) {
height: 6px;
}
.properties :global(.ant-select-single.ant-select-sm:not(.ant-select-customize-input) .ant-select-selector) {
padding: 0 4px 0 6px ;
}
.properties :global(.ant-select-single.ant-select-sm:not(.ant-select-customize-input).ant-select-show-arrow .ant-select-selection-item),
.properties :global(.ant-select-single.ant-select-sm:not(.ant-select-customize-input).ant-select-show-arrow .ant-select-selection-placeholder) {
padding-right: 10px;
}
.properties :global(.ant-select .ant-select-arrow) {
font-size: 10px;
inset-inline-end: 6px;
}

View File

@@ -90,7 +90,7 @@ export const nodeLibrary: NodeLibrary[] = [
type: "end", icon: endIcon,
config: {
output: {
type: 'define'
type: 'editor'
}
}
},
@@ -125,6 +125,7 @@ export const nodeLibrary: NodeLibrary[] = [
},
context: {
type: 'variableList',
placeholder: 'workflow.config.llm.contextPlaceholder'
},
messages: {
type: 'define',
@@ -134,7 +135,8 @@ export const nodeLibrary: NodeLibrary[] = [
content: undefined,
readonly: true
},
]
],
placeholder: 'workflow.config.llm.messagesPlaceholder'
},
memory: {
type: 'memoryConfig',
@@ -170,7 +172,8 @@ export const nodeLibrary: NodeLibrary[] = [
},
text: {
type: 'variableList',
filterLoopIterationVars: true
filterLoopIterationVars: true,
placeholder: 'workflow.config.parameter-extractor.textPlaceholder'
},
params: {
type: 'paramList',
@@ -178,6 +181,8 @@ export const nodeLibrary: NodeLibrary[] = [
prompt: {
type: 'messageEditor',
isArray: false,
titleVariant: 'borderless',
placeholder: 'workflow.config.parameter-extractor.promptPlaceholder'
},
}
}
@@ -189,7 +194,7 @@ export const nodeLibrary: NodeLibrary[] = [
{ type: "memory-read", icon: memoryReadIcon,
config: {
message: {
type: 'messageEditor',
type: 'editor',
isArray: false
},
config_id: {
@@ -212,7 +217,7 @@ export const nodeLibrary: NodeLibrary[] = [
{ type: "memory-write", icon: memoryWriteIcon,
config: {
message: {
type: 'messageEditor',
type: 'editor',
isArray: false
},
config_id: {
@@ -270,7 +275,8 @@ export const nodeLibrary: NodeLibrary[] = [
},
user_supplement_prompt: {
type: 'messageEditor',
isArray: false
isArray: false,
titleVariant: 'borderless'
}
}
},
@@ -436,6 +442,7 @@ export const nodeLibrary: NodeLibrary[] = [
type: 'messageEditor',
isArray: false,
enableJinja2: true,
titleVariant: 'borderless',
defaultValue: "{{arg1}}"
},
}

View File

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