Merge branch 'release/v0.2.8' into develop

* release/v0.2.8:
  fix(agent): Reading of docx multimodal files; Multimodal attachment history record
  fix(web): workflow header hidden operate
  feat(web):  multi_agent app not support share
  feat(web): chart content support files
  fix(web): update app export param key
  fix(web): app features bugfix
  fix(web): improve document preview handling for .doc files and validate docx format
  fix:pdf change version
  fix:cdn pdf
  fix: use real workflow_config id from db to avoid foreign key violation in workflow_executions
  fix: remove redundant local AppRelease import causing NameError in draft_run
  fix: shared app uses release snapshot config instead of draft in draft_run and get_agent_config
  fix: support both query param and body for new_name in copy_app for backward compatibility
  fix: read new_name from request body in copy_app endpoint
This commit is contained in:
Mark
2026-03-19 10:12:42 +08:00
22 changed files with 383 additions and 123 deletions

View File

@@ -194,6 +194,7 @@ def delete_app(
def copy_app( def copy_app(
app_id: uuid.UUID, app_id: uuid.UUID,
new_name: Optional[str] = None, new_name: Optional[str] = None,
payload: app_schema.CopyAppRequest = None,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user=Depends(get_current_user), current_user=Depends(get_current_user),
): ):
@@ -205,6 +206,8 @@ def copy_app(
- 不影响原应用 - 不影响原应用
""" """
workspace_id = current_user.current_workspace_id workspace_id = current_user.current_workspace_id
# body takes precedence over query param for backward compatibility
new_name = (payload.new_name if payload else None) or new_name
logger.info( logger.info(
"用户请求复制应用", "用户请求复制应用",
extra={ extra={
@@ -517,7 +520,7 @@ async def draft_run(
# 提前验证和准备(在流式响应开始前完成) # 提前验证和准备(在流式响应开始前完成)
from app.services.app_service import AppService from app.services.app_service import AppService
from app.services.multi_agent_service import MultiAgentService from app.services.multi_agent_service import MultiAgentService
from app.models import AgentConfig, ModelConfig from app.models import AgentConfig, ModelConfig, AppRelease
from sqlalchemy import select from sqlalchemy import select
from app.core.exceptions import BusinessException from app.core.exceptions import BusinessException
from app.services.draft_run_service import AgentRunService from app.services.draft_run_service import AgentRunService
@@ -556,6 +559,17 @@ async def draft_run(
service._check_agent_config(app_id) service._check_agent_config(app_id)
# 2. 获取 Agent 配置 # 2. 获取 Agent 配置
# 共享应用:从最新发布版本读配置快照,而非草稿
is_shared = app.workspace_id != workspace_id
if is_shared:
if not app.current_release_id:
raise BusinessException("该应用尚未发布,无法使用", BizCode.AGENT_CONFIG_MISSING)
release = db.get(AppRelease, app.current_release_id)
if not release:
raise BusinessException("发布版本不存在", BizCode.AGENT_CONFIG_MISSING)
agent_cfg = service._agent_config_from_release(release)
model_config = db.get(ModelConfig, release.default_model_config_id) if release.default_model_config_id else None
else:
stmt = select(AgentConfig).where(AgentConfig.app_id == app_id) stmt = select(AgentConfig).where(AgentConfig.app_id == app_id)
agent_cfg = db.scalars(stmt).first() agent_cfg = db.scalars(stmt).first()
if not agent_cfg: if not agent_cfg:
@@ -723,6 +737,16 @@ async def draft_run(
msg="多 Agent 任务执行成功" msg="多 Agent 任务执行成功"
) )
elif app.type == AppType.WORKFLOW: # 工作流 elif app.type == AppType.WORKFLOW: # 工作流
# 共享应用:从最新发布版本读配置快照,而非草稿
is_shared = app.workspace_id != workspace_id
if is_shared:
if not app.current_release_id:
raise BusinessException("该应用尚未发布,无法使用", BizCode.AGENT_CONFIG_MISSING)
release = db.get(AppRelease, app.current_release_id)
if not release:
raise BusinessException("发布版本不存在", BizCode.AGENT_CONFIG_MISSING)
config = service._workflow_config_from_release(release)
else:
config = workflow_service.check_config(app_id) config = workflow_service.check_config(app_id)
# 3. 流式返回 # 3. 流式返回
if payload.stream: if payload.stream:

View File

@@ -525,6 +525,13 @@ class AppRelease(BaseModel):
return int(dt.timestamp() * 1000) if dt else None return int(dt.timestamp() * 1000) if dt else None
# ---------- App Copy Schema ----------
class CopyAppRequest(BaseModel):
"""复制应用请求"""
new_name: Optional[str] = Field(None, description="新应用名称,不填则使用原名称-副本")
# ---------- App Share Schemas ---------- # ---------- App Share Schemas ----------
class AppShareCreate(BaseModel): class AppShareCreate(BaseModel):

View File

@@ -24,6 +24,7 @@ from app.services.model_service import ModelApiKeyService
from app.services.multi_agent_orchestrator import MultiAgentOrchestrator from app.services.multi_agent_orchestrator import MultiAgentOrchestrator
from app.services.multimodal_service import MultimodalService from app.services.multimodal_service import MultimodalService
from app.services.workflow_service import WorkflowService from app.services.workflow_service import WorkflowService
from app.schemas import FileType
logger = get_business_logger() logger = get_business_logger()
@@ -156,20 +157,6 @@ class AppChatService:
files=processed_files # 传递处理后的文件 files=processed_files # 传递处理后的文件
) )
# 保存消息
message_id = self.conversation_service.save_conversation_messages(
conversation_id=conversation_id,
user_message=message,
assistant_message=result["content"],
meta_data={
"usage": result.get("usage", {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0
})
}
)
ModelApiKeyService.record_api_key_usage(self.db, api_key_obj.id) ModelApiKeyService.record_api_key_usage(self.db, api_key_obj.id)
elapsed_time = time.time() - start_time elapsed_time = time.time() - start_time
@@ -191,6 +178,40 @@ class AppChatService:
tenant_id=tenant_id, workspace_id=workspace_id tenant_id=tenant_id, workspace_id=workspace_id
) )
# 构建用户消息内容(含多模态文件)
human_meta = {
"files": []
}
assistant_meta = {
"model": api_key_obj.model_name,
"usage": result.get("usage", {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}),
"audio_url": None
}
if files:
for f in files:
# url = await MultimodalService(self.db).get_file_url(f)
human_meta["files"].append({
"type": FileType.IMAGE,
"url": f.url
})
# 保存消息
if audio_url:
assistant_meta["audio_url"] = audio_url
self.conversation_service.add_message(
conversation_id=conversation_id,
role="user",
content=message,
meta_data=human_meta
)
ai_message = self.conversation_service.add_message(
conversation_id=conversation_id,
role="assistant",
content=result["content"],
meta_data=assistant_meta
)
message_id = ai_message.id
return { return {
"conversation_id": conversation_id, "conversation_id": conversation_id,
"message_id": str(message_id), "message_id": str(message_id),
@@ -344,24 +365,6 @@ class AppChatService:
elapsed_time = time.time() - start_time elapsed_time = time.time() - start_time
# 保存消息
self.conversation_service.add_message(
conversation_id=conversation_id,
role="user",
content=message
)
self.conversation_service.add_message(
message_id=message_id,
conversation_id=conversation_id,
role="assistant",
content=full_content,
meta_data={
"model": api_key_obj.model_name,
"usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": total_tokens}
}
)
ModelApiKeyService.record_api_key_usage(self.db, api_key_obj.id) ModelApiKeyService.record_api_key_usage(self.db, api_key_obj.id)
# 发送结束事件(包含 suggested_questions、tts、citations # 发送结束事件(包含 suggested_questions、tts、citations
@@ -373,13 +376,48 @@ class AppChatService:
{"model_name": api_key_obj.model_name, "api_key": api_key_obj.api_key, {"model_name": api_key_obj.model_name, "api_key": api_key_obj.api_key,
"api_base": api_key_obj.api_base}, {} "api_base": api_key_obj.api_base}, {}
) )
end_data["audio_url"] = await self.agent_service._generate_tts( stream_audio_url = await self.agent_service._generate_tts(
features_config, full_content, features_config, full_content,
{"model_name": api_key_obj.model_name, "api_key": api_key_obj.api_key, {"model_name": api_key_obj.model_name, "api_key": api_key_obj.api_key,
"api_base": api_key_obj.api_base, "provider": api_key_obj.provider}, "api_base": api_key_obj.api_base, "provider": api_key_obj.provider},
tenant_id=tenant_id, workspace_id=workspace_id tenant_id=tenant_id, workspace_id=workspace_id
) )
end_data["audio_url"] = stream_audio_url
end_data["citations"] = self.agent_service._filter_citations(features_config, []) end_data["citations"] = self.agent_service._filter_citations(features_config, [])
# 保存消息
human_meta = {
"files":[]
}
assistant_meta = {
"model": api_key_obj.model_name,
"usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": total_tokens},
"audio_url": None
}
if files:
for f in files:
# url = await MultimodalService(self.db).get_file_url(f)
human_meta["files"].append({
"type": FileType.IMAGE,
"url": f.url
})
if stream_audio_url:
assistant_meta["audio_url"] = stream_audio_url
self.conversation_service.add_message(
conversation_id=conversation_id,
role="user",
content=message,
meta_data=human_meta
)
self.conversation_service.add_message(
message_id=message_id,
conversation_id=conversation_id,
role="assistant",
content=full_content,
meta_data=assistant_meta
)
yield f"event: end\ndata: {json.dumps(end_data, ensure_ascii=False)}\n\n" yield f"event: end\ndata: {json.dumps(end_data, ensure_ascii=False)}\n\n"
logger.info( logger.info(

View File

@@ -1426,6 +1426,50 @@ class AppService:
logger.info("Agent 配置更新成功", extra={"app_id": str(app_id)}) logger.info("Agent 配置更新成功", extra={"app_id": str(app_id)})
return agent_cfg return agent_cfg
def _agent_config_from_release(self, release: "AppRelease") -> "AgentConfig":
"""从发布版本快照重建 AgentConfig 对象(不入库,仅用于运行)"""
cfg = release.config or {}
now = release.created_at or datetime.datetime.now()
agent_cfg = AgentConfig(
id=uuid.uuid4(),
app_id=release.app_id,
system_prompt=cfg.get("system_prompt", ""),
default_model_config_id=release.default_model_config_id,
model_parameters=cfg.get("model_parameters"),
knowledge_retrieval=cfg.get("knowledge_retrieval"),
memory=cfg.get("memory", {}),
variables=cfg.get("variables", []),
tools=cfg.get("tools", []),
skills=cfg.get("skills", {}),
features=cfg.get("features", {}),
is_active=True,
created_at=now,
updated_at=now,
)
return agent_cfg
def _workflow_config_from_release(self, release: "AppRelease") -> "WorkflowConfig":
"""从发布版本快照重建 WorkflowConfig 对象(不入库,仅用于运行)"""
cfg = release.config or {}
now = release.created_at or datetime.datetime.now()
from app.models.workflow_model import WorkflowConfig as WorkflowConfigModel
# 查出源应用真实的 WorkflowConfig id供 workflow_executions 外键使用
real_config = WorkflowConfigRepository(self.db).get_by_app_id(release.app_id)
real_id = real_config.id if real_config else uuid.uuid4()
wf_cfg = WorkflowConfigModel(
id=real_id,
app_id=release.app_id,
nodes=cfg.get("nodes", []),
edges=cfg.get("edges", []),
variables=cfg.get("variables", []),
execution_config=cfg.get("execution_config", {}),
triggers=cfg.get("triggers", []),
is_active=True,
created_at=now,
updated_at=now,
)
return wf_cfg
def get_agent_config( def get_agent_config(
self, self,
*, *,
@@ -1457,6 +1501,15 @@ class AppService:
# 只读操作,允许访问共享应用 # 只读操作,允许访问共享应用
self._validate_app_accessible(app, workspace_id) self._validate_app_accessible(app, workspace_id)
# 共享应用:返回最新发布版本的配置快照,而非草稿
if workspace_id and app.workspace_id != workspace_id:
if not app.current_release_id:
raise BusinessException("该应用尚未发布,无法使用", BizCode.AGENT_CONFIG_MISSING)
release = self.db.get(AppRelease, app.current_release_id)
if not release:
raise BusinessException("发布版本不存在", BizCode.AGENT_CONFIG_MISSING)
return self._agent_config_from_release(release)
stmt = select(AgentConfig).where( stmt = select(AgentConfig).where(
AgentConfig.app_id == app_id, AgentConfig.app_id == app_id,
AgentConfig.is_active.is_(True) AgentConfig.is_active.is_(True)
@@ -1555,6 +1608,16 @@ class AppService:
# 只读操作,允许访问共享应用 # 只读操作,允许访问共享应用
self._validate_app_accessible(app, workspace_id) self._validate_app_accessible(app, workspace_id)
# 共享应用:返回最新发布版本的配置快照,而非草稿
if workspace_id and app.workspace_id != workspace_id:
if not app.current_release_id:
raise BusinessException("该应用尚未发布,无法使用", BizCode.CONFIG_MISSING)
release = self.db.get(AppRelease, app.current_release_id)
if not release:
raise BusinessException("发布版本不存在", BizCode.CONFIG_MISSING)
return self._workflow_config_from_release(release)
repo = WorkflowConfigRepository(self.db) repo = WorkflowConfigRepository(self.db)
config = repo.get_by_app_id(app_id) config = repo.get_by_app_id(app_id)
if config: if config:

View File

@@ -37,6 +37,7 @@ from app.services.model_parameter_merger import ModelParameterMerger
from app.services.model_service import ModelApiKeyService from app.services.model_service import ModelApiKeyService
from app.services.multimodal_service import MultimodalService from app.services.multimodal_service import MultimodalService
from app.services.tool_service import ToolService from app.services.tool_service import ToolService
from app.schemas import FileType
logger = get_business_logger() logger = get_business_logger()
@@ -636,7 +637,13 @@ class AgentRunService:
ModelApiKeyService.record_api_key_usage(self.db, api_key_config.get("api_key_id")) ModelApiKeyService.record_api_key_usage(self.db, api_key_config.get("api_key_id"))
# 9. 保存会话消息 # 9. 生成 TTS audio_url在保存消息前生成以便一并存入 meta_data
audio_url = await self._generate_tts(
features_config, result["content"], api_key_config,
tenant_id=tenant_id, workspace_id=workspace_id
) if not sub_agent else None
# 10. 保存会话消息
if not sub_agent: if not sub_agent:
await self._save_conversation_message( await self._save_conversation_message(
conversation_id=conversation_id, conversation_id=conversation_id,
@@ -650,7 +657,9 @@ class AgentRunService:
"completion_tokens": 0, "completion_tokens": 0,
"total_tokens": 0 "total_tokens": 0
}) })
} },
files=files,
audio_url=audio_url
) )
response = { response = {
@@ -666,10 +675,7 @@ class AgentRunService:
features_config, result["content"], api_key_config, effective_params features_config, result["content"], api_key_config, effective_params
) if not sub_agent else [], ) if not sub_agent else [],
"citations": self._filter_citations(features_config, result.get("citations", [])), "citations": self._filter_citations(features_config, result.get("citations", [])),
"audio_url": await self._generate_tts( "audio_url": audio_url,
features_config, result["content"], api_key_config,
tenant_id=tenant_id, workspace_id=workspace_id
) if not sub_agent else None,
} }
logger.info( logger.info(
@@ -878,7 +884,13 @@ class AgentRunService:
"total_tokens": total_tokens "total_tokens": total_tokens
}) })
# 10. 保存会话消息 # 10. 生成 audio_url在保存消息前生成以便一并存入 meta_data
stream_audio_url = await self._generate_tts(
features_config, full_content, api_key_config,
tenant_id=tenant_id, workspace_id=workspace_id
) if not sub_agent else None
# 11. 保存会话消息
if not sub_agent: if not sub_agent:
await self._save_conversation_message( await self._save_conversation_message(
conversation_id=conversation_id, conversation_id=conversation_id,
@@ -888,10 +900,12 @@ class AgentRunService:
user_id=user_id, user_id=user_id,
meta_data={ meta_data={
"usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": total_tokens} "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": total_tokens}
} },
files=files,
audio_url=stream_audio_url
) )
# 11. 发送结束事件(包含 suggested_questions 和 tts # 12. 发送结束事件(包含 suggested_questions 和 tts
end_data: Dict[str, Any] = { end_data: Dict[str, Any] = {
"conversation_id": conversation_id, "conversation_id": conversation_id,
"elapsed_time": elapsed_time, "elapsed_time": elapsed_time,
@@ -901,10 +915,7 @@ class AgentRunService:
end_data["suggested_questions"] = await self._generate_suggested_questions( end_data["suggested_questions"] = await self._generate_suggested_questions(
features_config, full_content, api_key_config, effective_params features_config, full_content, api_key_config, effective_params
) )
end_data["audio_url"] = await self._generate_tts( end_data["audio_url"] = stream_audio_url
features_config, full_content, api_key_config,
tenant_id=tenant_id, workspace_id=workspace_id
)
end_data["citations"] = self._filter_citations(features_config, []) end_data["citations"] = self._filter_citations(features_config, [])
yield self._format_sse_event("end", end_data) yield self._format_sse_event("end", end_data)
@@ -1143,7 +1154,9 @@ class AgentRunService:
assistant_message: str, assistant_message: str,
meta_data: dict, meta_data: dict,
app_id: Optional[uuid.UUID] = None, app_id: Optional[uuid.UUID] = None,
user_id: Optional[str] = None user_id: Optional[str] = None,
files: Optional[List[FileInput]] = None,
audio_url: Optional[str] = None
) -> None: ) -> None:
"""保存会话消息(会话已通过 _ensure_conversation 确保存在) """保存会话消息(会话已通过 _ensure_conversation 确保存在)
@@ -1162,13 +1175,26 @@ class AgentRunService:
conv_uuid = uuid.UUID(conversation_id) conv_uuid = uuid.UUID(conversation_id)
# 保存消息(会话已经存在) # 保存消息(会话已经存在)
human_meta = {
"files": []
}
if files:
for f in files:
# url = await MultimodalService(self.db).get_file_url(f)
human_meta["files"].append({
"type": FileType.IMAGE,
"url": f.url
})
# 保存用户消息 # 保存用户消息
conversation_service.add_message( conversation_service.add_message(
conversation_id=conv_uuid, conversation_id=conv_uuid,
role="user", role="user",
content=user_message content=user_message,
meta_data=human_meta
) )
# 保存助手消息 # 保存助手消息(含 audio_url
if audio_url:
meta_data["audio_url"] = audio_url
conversation_service.add_message( conversation_service.add_message(
conversation_id=conv_uuid, conversation_id=conv_uuid,
role="assistant", role="assistant",

View File

@@ -41,7 +41,8 @@ TEXT_MIME = ['text/plain', 'text/x-markdown']
PDF_MIME = ['application/pdf'] PDF_MIME = ['application/pdf']
DOC_MIME = [ DOC_MIME = [
'application/msword', 'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/zip'
] ]
XLSX_MIME = [ XLSX_MIME = [
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
@@ -590,7 +591,7 @@ class MultimodalService:
return file_content.decode("utf-8") return file_content.decode("utf-8")
elif file_mime_type in PDF_MIME: elif file_mime_type in PDF_MIME:
return await self._extract_pdf_text(file_content) return await self._extract_pdf_text(file_content)
elif file_mime_type in DOC_MIME: elif file_mime_type in DOC_MIME and file.file_type.endswith(('docx', 'doc')):
return await self._extract_word_text(file_content) return await self._extract_word_text(file_content)
elif file_mime_type in XLSX_MIME and file.file_type.endswith(("xlsx", "xls")): elif file_mime_type in XLSX_MIME and file.file_type.endswith(("xlsx", "xls")):
return await self._extract_xlsx_text(file_content) return await self._extract_xlsx_text(file_content)

View File

@@ -46,7 +46,7 @@
"lexical": "^0.39.0", "lexical": "^0.39.0",
"mammoth": "^1.12.0", "mammoth": "^1.12.0",
"mermaid": "^11.12.1", "mermaid": "^11.12.1",
"pdfjs-dist": "^4.4.168", "pdfjs-dist": "4.10.38",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-i18next": "^15.0.0", "react-i18next": "^15.0.0",

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 13:59:45 * @Date: 2026-02-03 13:59:45
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-13 17:07:54 * @Last Modified time: 2026-03-18 20:01:29
*/ */
import { request } from '@/utils/request' import { request } from '@/utils/request'
import type { ApplicationModalData } from '@/views/ApplicationManagement/types' import type { ApplicationModalData } from '@/views/ApplicationManagement/types'
@@ -137,7 +137,7 @@ export const getExperienceConfig = (share_token: string) => {
}) })
} }
// Export application // Export application
export const appExport = (app_id: string, appName: string, data?: { release_version: string }) => { export const appExport = (app_id: string, appName: string, data?: { release_id: string }) => {
return request.getDownloadFile(`/apps/${app_id}/export`, `${appName}.yml`, data) return request.getDownloadFile(`/apps/${app_id}/export`, `${appName}.yml`, data)
} }
// Import application // Import application

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2025-12-10 16:46:17 * @Date: 2025-12-10 16:46:17
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-17 14:11:24 * @Last Modified time: 2026-03-18 20:48:03
*/ */
import { type FC, useRef, useEffect, useState } from 'react' import { type FC, useRef, useEffect, useState } from 'react'
import clsx from 'clsx' import clsx from 'clsx'
@@ -33,7 +33,7 @@ const ChatContent: FC<ChatContentProps> = ({
const audioRef = useRef<HTMLAudioElement | null>(null) const audioRef = useRef<HTMLAudioElement | null>(null)
const [playingIndex, setPlayingIndex] = useState<number | null>(null) const [playingIndex, setPlayingIndex] = useState<number | null>(null)
const handlePlay = (index: number, audioUrl: string) => { const handlePlay = (index: number, audio_url: string) => {
if (playingIndex === index) { if (playingIndex === index) {
audioRef.current?.pause() audioRef.current?.pause()
setPlayingIndex(null) setPlayingIndex(null)
@@ -42,7 +42,7 @@ const ChatContent: FC<ChatContentProps> = ({
if (audioRef.current) { if (audioRef.current) {
audioRef.current.pause() audioRef.current.pause()
} }
const audio = new Audio(audioUrl) const audio = new Audio(audio_url)
audioRef.current = audio audioRef.current = audio
audio.play() audio.play()
setPlayingIndex(index) setPlayingIndex(index)
@@ -108,6 +108,48 @@ const ChatContent: FC<ChatContentProps> = ({
{labelFormat(item)} {labelFormat(item)}
</div> </div>
} }
{item.meta_data?.files && item.meta_data?.files.length > 0 && <div>
{item.meta_data?.files?.map((file) => {
if (file.type.includes('image')) {
return (
<div key={file.url || file.uid} className={`rb:inline-block rb:group rb:relative rb:rounded-lg ${contentClassNames}`}>
<img src={file.url} alt={file.name} className="rb:w-full rb:max-w-80 rb:rounded-lg rb:object-cover rb:cursor-pointer" />
</div>
)
}
if (file.type.includes('video')) {
return (
<div key={file.url || file.uid} className="rb:w-45 rb:h-16 rb:inline-block rb:group rb:relative rb:rounded-lg">
<video src={file.url} controls className="rb:w-45 rb:h-16 rb:rounded-lg rb:object-cover rb:cursor-pointer" />
</div>
)
}
if (file.type.includes('audio')) {
return (
<div key={file.url || file.uid} className="rb:w-45 rb:h-16 rb:inline-flex rb:items-center rb:group rb:relative rb:rounded-lg rb:bg-[#F0F3F8] rb:py-2 rb:px-2.5 rb:gap-2">
<audio src={file.url} controls className="rb:w-45 rb:h-16" />
</div>
)
}
return (
<div key={file.url || file.uid} className="rb:w-45 rb:text-[12px] rb:gap-2.5 rb:flex rb:items-center rb:group rb:relative rb:rounded-lg rb:bg-[#F0F3F8] rb:py-2 rb:px-2.5">
{(file.type.includes('doc') || file.type.includes('docx') || file.type.includes('word') || file.type.includes('wordprocessingml.document')) && <div
className="rb:size-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/word_disabled.svg')] rb:hover:bg-[url('@/assets/images/conversation/word.svg')]"
></div>}
{(file.type.includes('pdf')) && <div
className="rb:size-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/pdf_disabled.svg')] rb:hover:bg-[url('@/assets/images/conversation/pdf.svg')]"
></div>}
{(file.type.includes('excel') || file.type.includes('spreadsheetml.sheet') || file.type.includes('csv')) && <div
className="rb:size-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/excel_disabled.svg')] rb:hover:bg-[url('@/assets/images/conversation/excel.svg')]"
></div>}
<div className="rb:flex-1 rb:w-32.5">
<div className="rb:leading-4 rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">{file.name}</div>
<div className="rb:leading-3.5 rb:mt-0.5 rb:text-[#5B6167] rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">{file.type} · {file.size}</div>
</div>
</div>
)
})}
</div>}
{/* Message bubble */} {/* Message bubble */}
<div className={clsx('rb:border rb:text-left rb:rounded-lg rb:mt-1.5 rb:leading-4.5 rb:p-[10px_12px_2px_12px] rb:inline-block rb:max-w-130 rb:wrap-break-word', contentClassNames, { <div className={clsx('rb:border rb:text-left rb:rounded-lg rb:mt-1.5 rb:leading-4.5 rb:p-[10px_12px_2px_12px] rb:inline-block rb:max-w-130 rb:wrap-break-word', contentClassNames, {
// Error message style (content is null and not assistant message) // Error message style (content is null and not assistant message)
@@ -121,14 +163,14 @@ const ChatContent: FC<ChatContentProps> = ({
{/* Render message content using Markdown component */} {/* Render message content using Markdown component */}
<Markdown content={renderRuntime ? item.content ?? '' : item.content ?? errorDesc ?? ''} /> <Markdown content={renderRuntime ? item.content ?? '' : item.content ?? errorDesc ?? ''} />
{item.audioUrl && <> {item.meta_data?.audio_url && <>
<Divider className="rb:my-3!" /> <Divider className="rb:my-3!" />
<Space size={12} className="rb:pb-2 rb:pl-1"> <Space size={12} className="rb:pb-2 rb:pl-1">
{playingIndex !== index {playingIndex !== index
? <SoundOutlined className="rb:cursor-pointer rb:hover:text-[#155EEF]! rb:size-5.5" onClick={() => handlePlay(index, item.audioUrl!)} /> ? <SoundOutlined className="rb:cursor-pointer rb:hover:text-[#155EEF]! rb:size-5.5" onClick={() => handlePlay(index, item.meta_data?.audio_url!)} />
: <div : <div
className="rb:size-5.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/audio_ing.gif')]" className="rb:size-5.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/audio_ing.gif')]"
onClick={() => handlePlay(index, item.audioUrl!)} onClick={() => handlePlay(index, item.meta_data?.audio_url!)}
/> />
} }
</Space> </Space>

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2025-12-10 16:45:54 * @Date: 2025-12-10 16:45:54
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-17 13:46:24 * @Last Modified time: 2026-03-18 20:47:42
*/ */
import { type ReactNode } from 'react' import { type ReactNode } from 'react'
@@ -22,9 +22,11 @@ export interface ChatItem {
created_at?: number | string; created_at?: number | string;
status?: string; status?: string;
subContent?: Record<string, any>[]; subContent?: Record<string, any>[];
files?: any[];
error?: string; error?: string;
audioUrl?: string; meta_data?: {
audio_url?: string;
files?: any[];
},
} }
/** /**

View File

@@ -4,7 +4,7 @@
* @Author: yujiangping * @Author: yujiangping
* @Date: 2026-03-16 19:01:12 * @Date: 2026-03-16 19:01:12
* @LastEditors: yujiangping * @LastEditors: yujiangping
* @LastEditTime: 2026-03-17 16:19:45 * @LastEditTime: 2026-03-18 18:35:53
*/ */
import { useState, useEffect, useRef, useCallback, type FC } from 'react'; import { useState, useEffect, useRef, useCallback, type FC } from 'react';
import { Spin, Alert, Button, Table, InputNumber, Image } from 'antd'; import { Spin, Alert, Button, Table, InputNumber, Image } from 'antd';
@@ -21,10 +21,9 @@ import { cookieUtils } from '@/utils/request';
import mammoth from 'mammoth'; import mammoth from 'mammoth';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.mjs?url';
// 设置 pdf.js worker // 设置 pdf.js worker - 使用 CDN 避免 Vite 打包动态 import 问题
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker; pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.10.38/pdf.worker.min.mjs';
interface DocumentPreviewProps { interface DocumentPreviewProps {
fileUrl: string; fileUrl: string;
@@ -287,7 +286,20 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
setError(false); setError(false);
setErrorMessage(''); setErrorMessage('');
try { try {
// .doc 旧格式 mammoth 不支持,使用 Office Online Viewer
if (getFileExtension() === '.doc') {
setHtmlContent('');
setLoading(false);
return;
}
const arrayBuffer = await fetchFileBuffer(fileUrl); const arrayBuffer = await fetchFileBuffer(fileUrl);
// 校验是否为有效的 docxZIP 格式,前两字节为 PK
const header = new Uint8Array(arrayBuffer.slice(0, 4));
if (header[0] !== 0x50 || header[1] !== 0x4B) {
// 不是 ZIP/docx 格式,可能是 HTML 错误页或 JSON 响应
const text = new TextDecoder().decode(arrayBuffer.slice(0, 200));
throw new Error(`文件内容不是有效的 docx 格式: ${text.substring(0, 100)}`);
}
const result = await mammoth.convertToHtml({ arrayBuffer }); const result = await mammoth.convertToHtml({ arrayBuffer });
setHtmlContent(result.value); setHtmlContent(result.value);
setLoading(false); setLoading(false);
@@ -492,12 +504,22 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
{/* Word 预览 */} {/* Word 预览 */}
{isWordFile() && !error && !loading && ( {isWordFile() && !error && !loading && (
getFileExtension() === '.doc' ? (
/* .doc 旧格式前端无法解析,提示下载 */
<div className="rb:w-full rb:flex-1 rb:flex rb:items-center rb:justify-center rb:bg-gray-50">
<div className="rb:text-center">
<p className="rb:text-gray-600 rb:mb-4">.doc 线</p>
<Button icon={<DownloadOutlined />} type="primary" onClick={handleDownload}></Button>
</div>
</div>
) : (
<div className="rb:w-full rb:flex-1 rb:overflow-auto rb:bg-white rb:p-6 rb:rounded rb:border rb:border-gray-200"> <div className="rb:w-full rb:flex-1 rb:overflow-auto rb:bg-white rb:p-6 rb:rounded rb:border rb:border-gray-200">
<div <div
className="rb:prose rb:max-w-none" className="rb:prose rb:max-w-none"
dangerouslySetInnerHTML={{ __html: htmlContent }} dangerouslySetInnerHTML={{ __html: htmlContent }}
/> />
</div> </div>
)
)} )}
{/* Excel 预览 */} {/* Excel 预览 */}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 16:29:33 * @Date: 2026-02-03 16:29:33
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-17 14:48:57 * @Last Modified time: 2026-03-18 19:49:09
*/ */
import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react' import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -30,7 +30,7 @@ import RadioGroupCard from '@/components/RadioGroupCard'
import { getModelListUrl } from '@/api/models' import { getModelListUrl } from '@/api/models'
import ModelConfigModal from './components/ModelConfigModal' import ModelConfigModal from './components/ModelConfigModal'
import type { Application } from '@/views/ApplicationManagement/types' import type { Application } from '@/views/ApplicationManagement/types'
import FeaturesConfig from './components/FeaturesConfig' // import FeaturesConfig from './components/FeaturesConfig'
const tagColors = ['processing', 'warning', 'default'] const tagColors = ['processing', 'warning', 'default']
const MAX_LENGTH = 5; const MAX_LENGTH = 5;
@@ -187,15 +187,15 @@ const Cluster = forwardRef<ClusterRef, { onFeaturesLoad?: (features: FeaturesCon
model_parameters: values model_parameters: values
}) })
} }
const handleSaveFeaturesConfig = (value: FeaturesConfigForm) => { // const handleSaveFeaturesConfig = (value: FeaturesConfigForm) => {
form.setFieldValue('features', value) // form.setFieldValue('features', value)
} // }
return ( return (
<Row className="rb:h-[calc(100vh-64px)]"> <Row className="rb:h-[calc(100vh-64px)]">
<Col span={12} className="rb:h-full rb:overflow-x-auto rb:border-r rb:border-[#DFE4ED] rb:p-[20px_16px_24px_16px]"> <Col span={12} className="rb:h-full rb:overflow-x-auto rb:border-r rb:border-[#DFE4ED] rb:p-[20px_16px_24px_16px]">
<Flex gap={10} justify="end" align="center" className="rb:mb-5!"> <Flex gap={10} justify="end" align="center" className="rb:mb-5!">
<FeaturesConfig value={values?.features as FeaturesConfigForm} refresh={handleSaveFeaturesConfig} /> {/* <FeaturesConfig value={values?.features as FeaturesConfigForm} refresh={handleSaveFeaturesConfig} /> */}
<Button type="primary" onClick={() => handleSave()}> <Button type="primary" onClick={() => handleSave()}>
{t('common.save')} {t('common.save')}
</Button> </Button>
@@ -295,6 +295,7 @@ const Cluster = forwardRef<ClusterRef, { onFeaturesLoad?: (features: FeaturesCon
value: type, value: type,
label: t(`application.${type}`), label: t(`application.${type}`),
}))} }))}
placeholder={t('common.pleaseSelect')}
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
@@ -306,6 +307,7 @@ const Cluster = forwardRef<ClusterRef, { onFeaturesLoad?: (features: FeaturesCon
value: type, value: type,
label: t(`application.${type}`), label: t(`application.${type}`),
}))} }))}
placeholder={t('common.pleaseSelect')}
/> />
</Form.Item> </Form.Item>
</Card>} </Card>}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 16:29:41 * @Date: 2026-02-03 16:29:41
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-18 14:30:41 * @Last Modified time: 2026-03-18 20:57:24
*/ */
import { type FC, useState, useEffect, useRef } from 'react'; import { type FC, useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -71,7 +71,7 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
} }
const handleExport = () => { const handleExport = () => {
if (!selectedVersion) return if (!selectedVersion) return
appExport(data.id, data.name, {release_version: selectedVersion.id}) appExport(data.id, data.name, { release_id: selectedVersion.id})
} }
return ( return (
<div className="rb:flex rb:h-[calc(100vh-64px)]"> <div className="rb:flex rb:h-[calc(100vh-64px)]">
@@ -132,7 +132,7 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
{data?.type !== 'multi_agent' && <Button onClick={handleExport}>{t('common.export')}</Button>} {data?.type !== 'multi_agent' && <Button onClick={handleExport}>{t('common.export')}</Button>}
{data.current_release_id !== selectedVersion.id && <Button onClick={handleRollback}>{t('application.willRollToThisVersion')}</Button>} {data.current_release_id !== selectedVersion.id && <Button onClick={handleRollback}>{t('application.willRollToThisVersion')}</Button>}
<Button type="primary" ghost onClick={() => releaseShareModalRef.current?.handleOpen()}>{t('application.share')}</Button> <Button type="primary" ghost onClick={() => releaseShareModalRef.current?.handleOpen()}>{t('application.share')}</Button>
<Button type="primary" ghost onClick={() => appSharingModalRef.current?.handleOpen()}>{t('application.sharing')}</Button> {data?.type !== 'multi_agent' && <Button type="primary" ghost onClick={() => appSharingModalRef.current?.handleOpen()}>{t('application.sharing')}</Button>}
</>} </>}
<Button type="primary" onClick={() => releaseModalRef.current?.handleOpen()}>{t('application.release')}</Button> <Button type="primary" onClick={() => releaseModalRef.current?.handleOpen()}>{t('application.release')}</Button>
</Space> </Space>

View File

@@ -1,3 +1,9 @@
/*
* @Author: ZhaoYing
* @Date: 2026-03-13 17:27:52
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-18 20:54:35
*/
import { type FC, useState, useRef, useEffect } from 'react' import { type FC, useState, useRef, useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { App } from 'antd' import { App } from 'antd'
@@ -116,7 +122,9 @@ const TestChat: FC<TestChatProps> = ({
role: 'user', role: 'user',
content: message, content: message,
created_at: Date.now(), created_at: Date.now(),
meta_data: {
files files
},
}]) }])
} }
@@ -136,7 +144,7 @@ const TestChat: FC<TestChatProps> = ({
const lastMsg = newList[newList.length - 1] const lastMsg = newList[newList.length - 1]
if (lastMsg.role === 'assistant') { if (lastMsg.role === 'assistant') {
lastMsg.content += content; lastMsg.content += content;
lastMsg.audioUrl = audio_url lastMsg.meta_data = {audio_url}
} }
return newList return newList
}) })
@@ -428,7 +436,7 @@ const TestChat: FC<TestChatProps> = ({
status, status,
error, error,
content: newList[lastIndex].content === '' ? null : newList[lastIndex].content, content: newList[lastIndex].content === '' ? null : newList[lastIndex].content,
audioUrl: audio_url meta_data: { audio_url: audio_url }
} }
} }
return newList return newList

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 16:27:39 * @Date: 2026-02-03 16:27:39
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-17 15:27:57 * @Last Modified time: 2026-03-18 20:52:33
*/ */
/** /**
* Chat debugging component for application testing * Chat debugging component for application testing
@@ -92,7 +92,9 @@ const Chat: FC<ChatProps> = ({
role: 'user', role: 'user',
content: message, content: message,
created_at: Date.now(), created_at: Date.now(),
meta_data: {
files files
},
}; };
updateChatList(prev => prev.map(item => ({ updateChatList(prev => prev.map(item => ({
...item, ...item,
@@ -142,7 +144,7 @@ const Chat: FC<ChatProps> = ({
{ {
...lastMsg, ...lastMsg,
content: lastMsg.content + (content || ''), content: lastMsg.content + (content || ''),
audioUrl: audio_url meta_data: { audio_url }
} }
] ]
} }

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 16:27:52 * @Date: 2026-02-03 16:27:52
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-18 15:40:53 * @Last Modified time: 2026-03-18 21:25:23
*/ */
import { type FC, useRef, useMemo, useCallback } from 'react'; import { type FC, useRef, useMemo, useCallback } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
@@ -183,6 +183,7 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
appRef?.current?.handleSaveFeaturesConfig?.(value) appRef?.current?.handleSaveFeaturesConfig?.(value)
onFeaturesChange?.(value) onFeaturesChange?.(value)
}, [appRef, onFeaturesChange]) }, [appRef, onFeaturesChange])
return ( return (
<> <>
<Header className="rb:w-full rb:h-16 rb:grid rb:grid-cols-3 rb:p-[16px_16px_16px_24px]! rb:border-b rb:border-[#EAECEE] rb:leading-8"> <Header className="rb:w-full rb:h-16 rb:grid rb:grid-cols-3 rb:p-[16px_16px_16px_24px]! rb:border-b rb:border-[#EAECEE] rb:leading-8">
@@ -211,9 +212,9 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
className={styles.tabs} className={styles.tabs}
/> />
</div> </div>
{application?.type === 'workflow' {application?.type === 'workflow' && source !== 'sharing'
? <div className="rb:h-8 rb:flex rb:items-center rb:justify-end rb:gap-2.5"> ? <div className="rb:h-8 rb:flex rb:items-center rb:justify-end rb:gap-2.5">
<FeaturesConfig source={application?.type} value={features} refresh={handleSaveFeaturesConfig} /> <FeaturesConfig source={application?.type} value={features as FeaturesConfigForm} refresh={handleSaveFeaturesConfig} />
<Button onClick={clear}>{t('workflow.clear')}</Button> <Button onClick={clear}>{t('workflow.clear')}</Button>
<Button onClick={addvariable}>{t('workflow.addvariable')}</Button> <Button onClick={addvariable}>{t('workflow.addvariable')}</Button>
<Button onClick={run}>{t('workflow.run')}</Button> <Button onClick={run}>{t('workflow.run')}</Button>

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-03-05 * @Date: 2026-03-05
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-17 18:10:47 * @Last Modified time: 2026-03-18 20:29:28
*/ */
import { forwardRef, useImperativeHandle, useState } from 'react'; import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, InputNumber, Flex, Switch, Row, Col, Radio } from 'antd'; import { Form, InputNumber, Flex, Switch, Row, Col, Radio } from 'antd';
@@ -27,22 +27,22 @@ const fileTypeOptions = [
{ {
type: 'document', type: 'document',
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/txt.svg')]"></div>, icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/txt.svg')]"></div>,
formats: 'TXT, MD, MDX, MARKDOWN, PDF, DOC, DOCX', formats: 'TXT, PDF, DOC, DOCX, XLSX, CSV, JSON',
}, },
{ {
type: 'image', type: 'image',
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/image.svg')]"></div>, icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/image.svg')]"></div>,
formats: 'JPG, JPEG, PNG, GIF, WEBP, SVG', formats: 'JPG, JPEG, PNG, GIF, WEBP',
}, },
{ {
type: 'audio', type: 'audio',
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/audio.svg')]"></div>, icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/audio.svg')]"></div>,
formats: 'MP3, M4A, WAV, AMR, MPGA', formats: 'MP3, M4A, WAV, OGG, FLAC',
}, },
{ {
type: 'video', type: 'video',
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/video.svg')]"></div>, icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/video.svg')]"></div>,
formats: 'MP4, MOV, MPEG, WEBM', formats: 'MP4, MOV, AVI, WEBM',
}, },
]; ];
@@ -80,7 +80,7 @@ const FileUploadSettingModal = forwardRef<FileUploadSettingModalRef, FileUploadS
const handleOpen = (values?: FileUpload) => { const handleOpen = (values?: FileUpload) => {
setVisible(true); setVisible(true);
if (values) { if (values) {
const methods = values.allowed_transfer_methods const methods = values.allowed_transfer_methods || ['local_file', 'remote_url']
const transferMethod = Array.isArray(methods) const transferMethod = Array.isArray(methods)
? methods.length === 2 ? 'both' : methods[0] ? methods.length === 2 ? 'both' : methods[0]
: methods : methods

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 16:34:12 * @Date: 2026-02-03 16:34:12
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-18 10:50:33 * @Last Modified time: 2026-03-18 21:00:12
*/ */
/** /**
* Application Management Page * Application Management Page
@@ -143,7 +143,7 @@ const ApplicationManagement: React.FC = () => {
<Form.Item name="type" noStyle> <Form.Item name="type" noStyle>
<Select <Select
placeholder={t('application.applicationType')} placeholder={t('application.applicationType')}
options={types.map((type) => ({ options={(activeTab === 'sharing' ? types.filter(type => type !== 'multi_agent') : types).map((type) => ({
value: type, value: type,
label: t(`application.${type}`), label: t(`application.${type}`),
}))} }))}
@@ -185,6 +185,7 @@ const ApplicationManagement: React.FC = () => {
<PageScrollList<Application, Query> <PageScrollList<Application, Query>
ref={scrollListRef} ref={scrollListRef}
url={getApplicationListUrl} url={getApplicationListUrl}
needLoading={false}
query={{ ...query, shared_only: activeTab === 'sharing', include_shared: activeTab !== 'apps' }} query={{ ...query, shared_only: activeTab === 'sharing', include_shared: activeTab !== 'apps' }}
renderItem={(item) => ( renderItem={(item) => (
<RbCard <RbCard

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-06 21:09:42 * @Date: 2026-02-06 21:09:42
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-17 14:42:31 * @Last Modified time: 2026-03-18 20:32:54
*/ */
/** /**
* File Upload Component * File Upload Component
@@ -71,6 +71,12 @@ const transform_file_type = {
'application/vnd.ms-powerpoint': 'document/ppt', 'application/vnd.ms-powerpoint': 'document/ppt',
'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'document/pptx', 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'document/pptx',
'application/vnd.ms-excel': 'document/xls',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'document/xlsx',
'text/csv': 'document/csv',
'application/json': 'document/json'
} }
// Mapping of file extensions to MIME types // Mapping of file extensions to MIME types
const ALL_FILE_TYPE: { const ALL_FILE_TYPE: {
@@ -88,6 +94,13 @@ const ALL_FILE_TYPE: {
ppt: 'application/vnd.ms-powerpoint', ppt: 'application/vnd.ms-powerpoint',
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
xls: 'application/vnd.ms-excel',
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
csv: 'text/csv',
json: 'application/json',
jpg: 'image/jpeg', jpg: 'image/jpeg',
jpeg: 'image/jpeg', jpeg: 'image/jpeg',
png: 'image/png', png: 'image/png',

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-06 21:09:47 * @Date: 2026-02-06 21:09:47
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-18 15:50:31 * @Last Modified time: 2026-03-18 21:10:01
*/ */
/** /**
* Upload File List Modal Component * Upload File List Modal Component
@@ -120,16 +120,16 @@ const UploadFileListModal = forwardRef<UploadFileListModalRef, UploadFileListMod
<Select <Select
placeholder={t('memoryConversation.fileType')} placeholder={t('memoryConversation.fileType')}
options={fileTypeOptions} options={fileTypeOptions}
className="rb:w-30" className="rb:w-30!"
/> />
</FormItem> </FormItem>
<FormItem <FormItem
{...restField} {...restField}
name={[name, 'url']} name={[name, 'url']}
rules={[{ required: true, message: t('common.pleaseEnter') }]} rules={[{ required: true, message: t('common.pleaseEnter') }]}
className="rb:mb-0!" className="rb:mb-0! rb:flex-1!"
> >
<Input placeholder={t('memoryConversation.fileUrl')} className="rb:w-82.5!" /> <Input placeholder={t('memoryConversation.fileUrl')} />
</FormItem> </FormItem>
<div <div
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]" className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 16:58:03 * @Date: 2026-02-03 16:58:03
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-18 15:35:05 * @Last Modified time: 2026-03-18 20:54:00
*/ */
/** /**
* Conversation Page * Conversation Page
@@ -160,7 +160,9 @@ const Conversation: FC = () => {
role: 'user', role: 'user',
content: message, content: message,
created_at: Date.now(), created_at: Date.now(),
meta_data: {
files files
},
}]) }])
} }
@@ -185,7 +187,7 @@ const Conversation: FC = () => {
{ {
...lastMsg, ...lastMsg,
content: lastMsg.content + content, content: lastMsg.content + content,
audioUrl: audio_url meta_data: { audio_url }
} }
] ]
} }

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-06 21:10:56 * @Date: 2026-02-06 21:10:56
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-18 14:34:20 * @Last Modified time: 2026-03-18 20:46:35
*/ */
/** /**
* Workflow Chat Component * Workflow Chat Component
@@ -63,9 +63,12 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
*/ */
const handleOpen = () => { const handleOpen = () => {
setOpen(true) setOpen(true)
if (data?.features) setFeatures(data.features)
} }
useEffect(() => {
if (data?.features && open) setFeatures(data.features)
}, [open, data?.features])
useEffect(() => { useEffect(() => {
if (open && graphRef.current && toolbarRef.current) { if (open && graphRef.current && toolbarRef.current) {
getVariables() getVariables()
@@ -148,10 +151,14 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
setLoading(true) setLoading(true)
const message = msg const message = msg
const files = toolbarRef.current?.getFiles() || []
setChatList(prev => [...prev, { setChatList(prev => [...prev, {
role: 'user', role: 'user',
content: message, content: message,
created_at: Date.now(), created_at: Date.now(),
meta_data: {
files
},
}]) }])
setChatList(prev => [...prev, { setChatList(prev => [...prev, {
role: 'assistant', role: 'assistant',
@@ -335,7 +342,6 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
}) })
} }
const files = toolbarRef.current?.getFiles() || []
setMessage(undefined) setMessage(undefined)
toolbarRef.current?.setFiles([]) toolbarRef.current?.setFiles([])
setFileList([]) setFileList([])