Merge branch 'refs/heads/release/v0.2.8' into fix/features_028

This commit is contained in:
Timebomb2018
2026-03-19 17:58:17 +08:00
11 changed files with 110 additions and 37 deletions

View File

@@ -172,7 +172,7 @@ def _retrieve_for_knowledge(
return results, chat_model, embedding_model
# Folder 类型:递归处理子知识库
if db_knowledge.type == knowledge_model.KnowledgeType.Folder:
if db_knowledge.type == knowledge_model.KnowledgeType.FOLDER:
children = knowledge_repository.get_knowledges_by_parent_id(db=db, parent_id=db_knowledge.id)
for child in children:
if not (child and child.chunk_num > 0 and child.status == 1):

View File

@@ -636,30 +636,33 @@ class WorkflowService:
final_messages = result.get("messages", [])[init_message_length:]
human_message = ""
assistant_message = ""
human_meta = {
"files": []
}
for message in final_messages:
if message["role"] == "user":
if isinstance(message["content"], str):
human_message += message["content"]
elif isinstance(message["content"], list):
for file in message["content"]:
if file.get("type") == FileType.IMAGE:
human_message += f"![image]({file.get('url', '')})"
else:
human_message += f"[{file.get('type')}]({file.get('url', '')})"
human_meta["files"].append({
"type": file.get("type"),
"url": file.get("url")
})
if message["role"] == "assistant":
assistant_message = message["content"]
self.conversation_service.add_message(
conversation_id=conversation_id_uuid,
role="user",
content=human_message,
meta_data=None
meta_data=human_meta
)
self.conversation_service.add_message(
message_id=message_id,
conversation_id=conversation_id_uuid,
role="assistant",
content=assistant_message,
meta_data={"usage": token_usage}
meta_data={"usage": token_usage, "audio_url": None}
)
self.update_execution_status(
execution.execution_id,

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-02 15:01:59
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-17 15:35:34
* @Last Modified time: 2026-03-19 13:41:26
*/
/**
@@ -42,7 +42,8 @@ const ButtonCheckbox: FC<ButtonCheckboxProps> = ({
icon,
checkedIcon,
children,
cicle = false
cicle = false,
disabled,
}) => {
// Listen to value changes and trigger side effects via onValueChange callback
useEffect(() => {
@@ -70,6 +71,7 @@ const ButtonCheckbox: FC<ButtonCheckboxProps> = ({
"rb:bg-[rgba(21,94,239,0.06)] rb:border-[rgba(21,94,239,0.25)] rb:hover:bg-[rgba(21,94,239,0.06)] rb:text-[#155EEF]": checked,
// Unchecked state: gray border and dark text
"rb:border-[#DFE4ED] rb:text-[#212332]": !checked,
"rb:opacity-65 rb:cursor-not-allowed!": disabled
})}
onClick={handleChange}
>

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2025-12-10 16:46:17
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-19 10:37:01
* @Last Modified time: 2026-03-19 13:38:20
*/
import { type FC, useRef, useEffect, useState } from 'react'
import clsx from 'clsx'
@@ -118,7 +118,7 @@ const ChatContent: FC<ChatContentProps> = ({
{labelFormat(item)}
</div>
}
{item.meta_data?.files && item.meta_data?.files.length > 0 && <Flex vertical align="end">
{item.meta_data?.files && item.meta_data?.files.length > 0 && <Flex gap={8} vertical align="end">
{item.meta_data?.files?.map((file) => {
if (file.type.includes('image')) {
return (

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2025-12-10 16:46:14
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-06 13:36:20
* @Last Modified time: 2026-03-19 16:05:56
*/
import { type FC, useEffect, useMemo } from 'react'
import { Flex, Input, Form } from 'antd'
@@ -109,15 +109,20 @@ const ChatInput: FC<ChatInputProps> = ({
}
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>}
{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>
: (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>
: null
}
<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>

View File

@@ -1564,6 +1564,7 @@ export const en = {
summary: 'Summary',
core_entities: 'Core Entities',
communityDetailEmptyDesc: 'Click on a community in the chart on the left to view details',
communityLoadingTip: 'Generating community graph',
},
space: {
createSpace: 'Create Space',

View File

@@ -1562,6 +1562,7 @@ export const zh = {
summary: '摘要',
core_entities: '核心实体',
communityDetailEmptyDesc: '点击左侧图表中的社区查看详情',
communityLoadingTip: '社区图谱生成中',
},
space: {
createSpace: '创建空间',

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:27:52
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-18 21:25:23
* @Last Modified time: 2026-03-19 17:13:54
*/
import { type FC, useRef, useMemo, useCallback } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
@@ -212,7 +212,7 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
className={styles.tabs}
/>
</div>
{application?.type === 'workflow' && source !== 'sharing'
{application?.type === 'workflow' && source !== 'sharing' && activeTab === 'arrangement'
? <div className="rb:h-8 rb:flex rb:items-center rb:justify-end rb:gap-2.5">
<FeaturesConfig source={application?.type} value={features as FeaturesConfigForm} refresh={handleSaveFeaturesConfig} />
<Button onClick={clear}>{t('workflow.clear')}</Button>

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-03-05
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-19 09:59:42
* @Last Modified time: 2026-03-19 15:18:20
*/
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, InputNumber, Flex, Switch, Row, Col, Radio } from 'antd';
@@ -27,22 +27,43 @@ const fileTypeOptions = [
{
type: 'document',
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/txt.svg')]"></div>,
formats: 'TXT, PDF, DOC, DOCX, XLSX, CSV, JSON',
formats: [
"pdf",
"docx",
"doc",
"xlsx",
"xls",
"txt",
"csv",
"json",
"md",
],
},
{
type: 'image',
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/image.svg')]"></div>,
formats: 'JPG, JPEG, PNG, GIF, WEBP',
formats: [
"png",
"jpg",
"jpeg"
],
},
{
type: 'audio',
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/audio.svg')]"></div>,
formats: 'MP3, M4A, WAV, OGG, FLAC',
formats: [
"mp3",
"wav",
"m4a",
],
},
{
type: 'video',
icon: <div className="rb:size-9 rb:bg-cover rb:bg-[url('@/assets/images/file/video.svg')]"></div>,
formats: 'MP4, MOV, AVI, WEBM',
formats: [
"mp4",
"mov",
],
},
];
@@ -50,16 +71,38 @@ const defaultValues: FileUpload = {
enabled: false,
image_enabled: false,
image_max_size_mb: 20,
image_allowed_extensions: ['png', 'jpg', 'jpeg', 'gif', 'webp'],
image_allowed_extensions: [
"png",
"jpg",
"jpeg"
],
audio_enabled: false,
audio_max_size_mb: 50,
audio_allowed_extensions: ['mp3', 'wav', 'm4a', 'ogg', 'flac'],
audio_allowed_extensions: [
"mp3",
"wav",
"m4a",
"ogg",
"flac"
],
document_enabled: false,
document_max_size_mb: 100,
document_allowed_extensions: ['pdf', 'docx', 'xlsx', 'txt', 'csv', 'json'],
document_allowed_extensions: [
"pdf",
"docx",
"xlsx",
"txt",
"csv",
"json"
],
video_enabled: false,
video_max_size_mb: 500,
video_allowed_extensions: ['mp4', 'mov', 'avi', 'webm'],
video_max_size_mb: 100,
video_allowed_extensions: [
"mp4",
"mov",
"avi",
"webm"
],
max_file_count: 5,
allowed_transfer_methods: 'both'
}
@@ -127,7 +170,7 @@ const FileUploadSettingModal = forwardRef<FileUploadSettingModalRef, FileUploadS
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mb-1">{t('application.maxCount')}</div>
<Form.Item label={t('application.maxCount')} name="max_file_count">
<InputNumber min={1} max={100} precision={0} className="rb:w-full!" placeholder={t('common.pleaseEnter')} />
<InputNumber min={1} max={20} precision={0} className="rb:w-full!" placeholder={t('common.pleaseEnter')} />
</Form.Item>
<Form.Item label={t('application.supportedTypes')}>
@@ -149,7 +192,7 @@ const FileUploadSettingModal = forwardRef<FileUploadSettingModalRef, FileUploadS
<Flex align="center" justify="space-between">
<Flex vertical>
<div className="rb:font-medium">{t(`application.${option.type}`)}</div>
<div className="rb:text-[12px] rb:text-[#5B6167]">{option.formats}</div>
<div className="rb:text-[12px] rb:text-[#5B6167]">{option.formats.map(item => item.toUpperCase()).join(', ')}</div>
</Flex>
<Form.Item name={enabledKey} valuePropName="checked" noStyle>
<Switch />
@@ -161,7 +204,7 @@ const FileUploadSettingModal = forwardRef<FileUploadSettingModalRef, FileUploadS
<Flex align="center" gap={12} className="rb:mt-3! rb:pt-3! rb:border-t rb:border-[#DFE4ED]">
<div>{t('application.singleMaxSize')}: </div>
<Form.Item name={sizeKey} noStyle>
<InputNumber min={1} max={500} suffix="MB" className="rb:flex-1" />
<InputNumber min={1} max={100} suffix="MB" className="rb:flex-1" />
</Form.Item>
<Form.Item name={`${option.type}_allowed_extensions`} hidden />
</Flex>

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:58:03
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-18 20:54:00
* @Last Modified time: 2026-03-19 12:30:41
*/
/**
* Conversation Page
@@ -63,6 +63,7 @@ const Conversation: FC = () => {
const [isHasMemory, setIsHasMemory] = useState(false)
const [memory, setMemory] = useState(true)
const [features, setFeatures] = useState<FeaturesConfigForm>({} as FeaturesConfigForm)
const [config, setConfig] = useState<Record<string, any>>({})
useEffect(() => {
const shareToken = localStorage.getItem(`shareToken_${token}`)
@@ -88,6 +89,7 @@ const Conversation: FC = () => {
.then(res => {
const response = res as { variables: Variable[]; features: FeaturesConfigForm; app_type: string; memory?: boolean; }
toolbarRef.current?.setVariables(response.variables || [])
setConfig(response)
setFeatures(response.features)
setIsHasMemory((response.app_type === 'workflow' && response.memory) || (response.app_type !== 'workflow'))
})
@@ -284,6 +286,7 @@ const Conversation: FC = () => {
}
const handleChangeMemory = (value: boolean) => {
if (config.app_type === 'workflow') return;
modal.confirm({
title: value ? t('memoryConversation.memoryTipTitle') : t('memoryConversation.memoryCancelTipTitle'),
okText: t('common.confirm'),
@@ -388,6 +391,7 @@ const Conversation: FC = () => {
icon={MemoryFunctionIcon}
checkedIcon={MemoryFunctionCheckedIcon}
checked={memory}
disabled={config.app_type === 'workflow'}
onChange={handleChangeMemory}
>
{t('memoryConversation.memory')}

View File

@@ -1,6 +1,8 @@
import React, { useState, type FC, useEffect } from 'react'
import { useParams } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { Spin, Flex } from 'antd';
import type { CommunityD3Node, CommunityGraphData, RawCommunityGraphData, RawCommunityNode } from '@/components/D3Graph/types'
import { buildCommunityGraphData } from '@/components/D3Graph/utils'
import CommunityGraph from '@/components/D3Graph/CommunityGraph'
@@ -40,14 +42,17 @@ const NodeTooltip: FC<{ node: CommunityD3Node }> = ({ node }) => {
const CommunityNetwork: FC<{ onSelectCommunity?: (node: RawCommunityNode) => void }> = ({ onSelectCommunity }) => {
const { id } = useParams()
const { t } = useTranslation()
const [graphData, setGraphData] = useState<CommunityGraphData | null>(null)
const [empty, setEmpty] = useState(false)
const [loading, setLoading] = useState(false)
useEffect(() => {
if (!id) return
const controller = new AbortController()
setEmpty(false)
setGraphData(null)
setLoading(true)
getMemoryCommunityGraph(id, { signal: controller.signal }).then(res => {
const raw = res as RawCommunityGraphData
if (!raw.nodes?.length) { setEmpty(true); return }
@@ -55,9 +60,18 @@ const CommunityNetwork: FC<{ onSelectCommunity?: (node: RawCommunityNode) => voi
if (!built) { setEmpty(true); return }
setGraphData(built)
}).catch((e) => { if (e?.code !== 'ERR_CANCELED') setEmpty(true) })
.finally(() => setLoading(false))
return () => controller.abort()
}, [id])
if (loading) {
return <Flex align="center" justify="center" className="rb:w-full rb:h-full">
<Spin tip={t('userMemory.communityLoadingTip')} size="large">
<div className="rb:w-64 rb:h-64" />
</Spin>
</Flex>
}
return (
<CommunityGraph
data={graphData}