Merge pull request #494 from SuanmoSuanyangTechnology/release/v0.2.6

Release/v0.2.6
This commit is contained in:
Ke Sun
2026-03-06 16:37:23 +08:00
committed by GitHub
17 changed files with 130 additions and 49 deletions

View File

@@ -371,6 +371,11 @@ def update_model(
if model_data.type is not None or model_data.provider is not None:
raise BusinessException("不允许更改模型类型和供应商", BizCode.INVALID_PARAMETER)
if model_data.is_active:
active_keys = ModelApiKeyService.get_api_keys_by_model(db=db, model_config_id=model_id, is_active=model_data.is_active)
if not active_keys:
raise BusinessException("请先为该模型配置可用的 API Key", BizCode.INVALID_PARAMETER)
try:
api_logger.debug(f"开始更新模型配置: model_id={model_id}")

View File

@@ -192,8 +192,10 @@ class Settings:
# Celery configuration (internal)
# NOTE: 变量名不以 CELERY_ 开头,避免被 Celery CLI 的前缀匹配机制劫持
# 详见 docs/celery-env-bug-report.md
REDIS_DB_CELERY_BROKER: int = int(os.getenv("REDIS_DB_CELERY_BROKER", "1"))
REDIS_DB_CELERY_BACKEND: int = int(os.getenv("REDIS_DB_CELERY_BACKEND", "2"))
# 默认使用 Redis DB 3 (broker) 和 DB 4 (backend),与业务缓存 (DB 1/2) 隔离
# 多人共用同一 Redis 时,每位开发者应在 .env 中配置不同的 DB 编号避免任务互相干扰
REDIS_DB_CELERY_BROKER: int = int(os.getenv("REDIS_DB_CELERY_BROKER", "3"))
REDIS_DB_CELERY_BACKEND: int = int(os.getenv("REDIS_DB_CELERY_BACKEND", "4"))
# SMTP Email Configuration
SMTP_SERVER: str = os.getenv("SMTP_SERVER", "smtp.gmail.com")

View File

@@ -23,6 +23,7 @@ class ModelConfigBase(BaseModel):
load_balance_strategy: Optional[str] = Field(LoadBalanceStrategy.NONE.value, description="负载均衡策略")
capability: List[str] = Field(default_factory=list, description="模型能力列表")
is_omni: bool = Field(False, description="是否为Omni模型")
model_id: Optional[uuid.UUID] = Field(None, description="基础模型ID")
class ApiKeyCreateNested(BaseModel):

View File

@@ -703,7 +703,7 @@ class AppService:
self.db.flush()
# 如果是 agent 类型,复制 AgentConfig
if source_app.type == "agent":
if source_app.type == AppType.AGENT:
source_config = self.db.query(AgentConfig).filter(
AgentConfig.app_id == source_app.id
).first()
@@ -725,6 +725,50 @@ class AppService:
)
self.db.add(new_config)
elif source_app.type == AppType.WORKFLOW:
source_config = self.db.query(WorkflowConfig).filter(
WorkflowConfig.app_id == source_app.id
).first()
if source_config:
new_config = WorkflowConfig(
id=uuid.uuid4(),
app_id=new_app.id,
nodes=source_config.nodes.copy() if source_config.nodes else [],
edges=source_config.edges.copy() if source_config.edges else [],
variables=source_config.variables.copy() if source_config.variables else [],
execution_config=source_config.execution_config.copy() if source_config.execution_config else {},
triggers=source_config.triggers.copy() if source_config.triggers else [],
is_active=True,
created_at=now,
updated_at=now,
)
self.db.add(new_config)
elif source_app.type == AppType.MULTI_AGENT:
source_config = self.db.query(MultiAgentConfig).filter(
MultiAgentConfig.app_id == source_app.id
).first()
if source_config:
new_config = MultiAgentConfig(
id=uuid.uuid4(),
app_id=new_app.id,
master_agent_id=source_config.master_agent_id,
master_agent_name=source_config.master_agent_name,
default_model_config_id=source_config.default_model_config_id,
model_parameters=source_config.model_parameters,
orchestration_mode=source_config.orchestration_mode,
sub_agents=source_config.sub_agents.copy() if source_config.sub_agents else [],
routing_rules=source_config.routing_rules.copy() if source_config.routing_rules else None,
execution_config=source_config.execution_config.copy() if source_config.execution_config else {},
aggregation_strategy=source_config.aggregation_strategy,
is_active=True,
created_at=now,
updated_at=now,
)
self.db.add(new_config)
self.db.commit()
self.db.refresh(new_app)

View File

@@ -780,6 +780,7 @@ class ModelBaseService:
"description": model_base.description,
"capability": model_base.capability,
"is_omni": model_base.is_omni,
"is_active": False,
"is_composite": False
}
model_config = ModelConfigRepository.create(db, model_config_data)

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2025-12-10 16:46:14
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-04 18:42:49
* @Last Modified time: 2026-03-06 13:36:20
*/
import { type FC, useEffect, useMemo } from 'react'
import { Flex, Input, Form } from 'antd'
@@ -50,13 +50,13 @@ const ChatInput: FC<ChatInputProps> = ({
const handleDelete = (file: any) => {
fileChange?.(fileList?.filter(item => item.uid !== file.uid) || [])
fileChange?.(fileList?.filter(item => file.url ? item.url !== file.url : item.uid !== file.uid) || [])
}
// Convert file object to preview URL
const previewFileList = useMemo(() => {
return fileList?.map(file => ({
...file,
url: file.url || (file.originFileObj ? URL.createObjectURL(file.originFileObj) : file.thumbUrl)
url: file.thumbUrl || file.url || (file.originFileObj ? URL.createObjectURL(file.originFileObj) : undefined)
})) || []
}, [fileList])
@@ -72,7 +72,7 @@ const ChatInput: FC<ChatInputProps> = ({
{previewFileList.map((file) => {
if (file.type.includes('image')) {
return (
<div key={file.uid} className="rb:inline-block rb:group rb:relative rb:rounded-lg">
<div key={file.url || file.uid} className="rb:inline-block rb:group rb:relative rb:rounded-lg">
<img src={file.url} alt={file.name} className="rb:size-12! rb:rounded-lg rb:object-cover rb:cursor-pointer" />
<div
className="rb:hidden rb:group-hover:block rb:absolute rb:-right-1 rb:-top-1 rb:size-3.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/delete.svg')] rb:hover:bg-[url('@/assets/images/conversation/delete_hover.svg')]"
@@ -83,7 +83,7 @@ const ChatInput: FC<ChatInputProps> = ({
}
if (file.type.includes('video')) {
return (
<div key={file.uid} className="rb:w-45 rb:h-16 rb:inline-block rb:group rb:relative rb:rounded-lg">
<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
className="rb:hidden rb:group-hover:block rb:absolute rb:-right-1 rb:-top-1 rb:size-3.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/delete.svg')] rb:hover:bg-[url('@/assets/images/conversation/delete_hover.svg')]"
@@ -94,7 +94,7 @@ const ChatInput: FC<ChatInputProps> = ({
}
if (file.type.includes('audio')) {
return (
<div key={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">
<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
className="rb:hidden rb:group-hover:block rb:absolute rb:-right-1 rb:-top-1 rb:size-3.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/delete.svg')] rb:hover:bg-[url('@/assets/images/conversation/delete_hover.svg')]"
@@ -104,7 +104,7 @@ const ChatInput: FC<ChatInputProps> = ({
)
}
return (
<div key={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">
<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>}

View File

@@ -1572,7 +1572,7 @@ export const en = {
intelligentSemanticPruningFunction: 'Intelligent Semantic Pruning Function',
intelligentSemanticPruningFunctionDesc: 'Whether to activate intelligent semantic pruning (true/false).',
intelligentSemanticPruningScene: 'Intelligent Semantic Pruning Scene',
intelligentSemanticPruningSceneDesc: 'Select intelligent semantic pruning scene (education, online_service, outbound).',
intelligentSemanticPruningSceneDesc: 'Semantic pruning scenarios are consistent with ontology engineering scenarios',
intelligentSemanticPruningThreshold: 'Intelligent Semantic Pruning Threshold',
intelligentSemanticPruningThresholdDesc: 'Set intelligent semantic pruning threshold (0-0.9).',
reflectionEngine: 'Self-Reflexion Engine',

View File

@@ -96,7 +96,7 @@ export const zh = {
createMemorySummary: '创建记忆摘要',
memoryManagement: '记忆管理',
spaceManagement: '空间管理',
memoryExtractionEngine: '记忆取引擎',
memoryExtractionEngine: '记忆取引擎',
forgettingEngine: '遗忘引擎',
apiKeyManagement: 'API KEY管理',
knowledgePrivate: '详情',
@@ -1283,7 +1283,7 @@ export const zh = {
createConfiguration: '创建配置',
editConfiguration: '编辑配置',
desc: '描述',
memoryExtractionEngine: '记忆取引擎',
memoryExtractionEngine: '记忆取引擎',
forgottenEngine: '遗忘引擎',
active: '活跃',
inactive: '不活跃',
@@ -1571,7 +1571,7 @@ export const zh = {
intelligentSemanticPruningFunction: '智能语义修剪功能',
intelligentSemanticPruningFunctionDesc: '是否激活智能语义修剪true/false。',
intelligentSemanticPruningScene: '智能语义修剪场景',
intelligentSemanticPruningSceneDesc: '选择智能语义修剪场景education、online_service、outbound',
intelligentSemanticPruningSceneDesc: '语义剪枝场景与本体工程场景一致',
intelligentSemanticPruningThreshold: '智能语义修剪阈值',
intelligentSemanticPruningThresholdDesc: '设置智能语义修剪阈值0-0.9)。',
reflectionEngine: '自我反思引擎',

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-28 14:08:14
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-02 17:39:49
* @Last Modified time: 2026-03-06 12:05:46
*/
/**
* UploadWorkflowModal Component
@@ -101,6 +101,7 @@ const UploadWorkflowModal = forwardRef<UploadWorkflowModalRef, UploadWorkflowMod
formData.append('platform', values.platform);
formData.append('file', values.file[0]);
setLoading(true)
// Call import workflow API
importWorkflow(formData)
.then(res => {
@@ -114,21 +115,24 @@ const UploadWorkflowModal = forwardRef<UploadWorkflowModalRef, UploadWorkflowMod
} else {
setCurrent(2);
// Pre-fill form with file information
const fileNameSplit = values.file[0].name.split('.')
form.setFieldsValue({
name: values.file[0].name.split('.')[0],
name: fileNameSplit.slice(0, fileNameSplit.length - 1).join('.'),
platform: values.platform,
fileName: values.file[0].name,
fileSize: values.file[0].size,
});
}
});
})
.finally(() => setLoading(false));
break;
case 1: // Step 2: Error/warning display
if (firstFormData) {
const { file, platform } = firstFormData;
const fileNameSplit = firstFormData.file[0].name.split('.')
// Pre-fill form with file information
form.setFieldsValue({
name: file[0].name.split('.')[0],
name: fileNameSplit.slice(0, fileNameSplit.length - 1).join('.'),
platform: platform,
fileName: file[0].name,
fileSize: file[0].size,
@@ -175,7 +179,9 @@ const UploadWorkflowModal = forwardRef<UploadWorkflowModalRef, UploadWorkflowMod
}
// Reset form if not going back to error/warning step
if (newStep !== 1) {
if (newStep === 0) {
form.setFieldsValue(firstFormData || {})
} else if (newStep !== 1) {
form.resetFields();
}
setCurrent(newStep);
@@ -186,14 +192,16 @@ const UploadWorkflowModal = forwardRef<UploadWorkflowModalRef, UploadWorkflowMod
* @param {string} type - Navigation type ('detail' or 'list')
*/
const handleJump = (type: string) => {
switch(type) {
case 'detail':
// Open application detail page in new tab
window.open(`/#/application/config/${appId}`, '_blank');
break;
}
refresh();
handleClose();
refresh();
setTimeout(() => {
switch (type) {
case 'detail':
// Open application detail page in new tab
window.open(`/#/application/config/${appId}`, '_blank');
break;
}
}, 100)
};
/**
@@ -350,7 +358,7 @@ const UploadWorkflowModal = forwardRef<UploadWorkflowModalRef, UploadWorkflowMod
title={t('application.importSuccess')}
subTitle={t('application.importSuccessDesc')}
extra={[
<Button key="back" onClick={() => handleJump('list')}>
<Button key="back" onClick={() => handleJump('list')}>
{t('application.gotoList')}
</Button>,
<Button

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-06 21:09:42
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-05 15:09:22
* @Last Modified time: 2026-03-06 12:20:43
*/
/**
* File Upload Component
@@ -208,6 +208,7 @@ const UploadFiles = forwardRef<UploadFilesRef, UploadFilesProps>(({
newFileList.map(file => {
const type = (file.type && transform_file_type[file.type as keyof typeof transform_file_type]) || file.type || 'document'
file.type = type
file.thumbUrl = file.thumbUrl || URL.createObjectURL(file.originFileObj as Blob)
})
setFileList(newFileList);
if (onChange) {

View File

@@ -672,9 +672,17 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
{currentType !== 'Folder' && dynamicTypeList.map((tp) => {
const fieldKey = typeToFieldKey(tp);
// When tp is 'llm', merge llm and chat options
const options = tp.toLowerCase() === 'llm' || tp.toLowerCase() === 'image2text'
let options = tp.toLowerCase() === 'llm' || tp.toLowerCase() === 'image2text'
? [...(modelOptionsByType['llm'] || []), ...(modelOptionsByType['chat'] || [])]
: modelOptionsByType[tp] || [];
// When tp is 'image2text', filter to only include models with 'vision' capability
if (tp.toLowerCase() === 'image2text') {
options = options.filter((opt: any) => {
const model = models?.items?.find((m: any) => m.id === opt.value);
return model?.capability?.includes('vision');
});
}
return (
<Form.Item
key={tp}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 17:30:06
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-04 10:09:45
* @Last Modified time: 2026-03-06 13:49:00
*/
/**
* Memory Extraction Engine Configuration Constants
@@ -140,13 +140,8 @@ export const configList: ConfigVo[] = [
{
label: 'intelligentSemanticPruningScene',
variableName: 'pruning_scene',
control: 'select',
control: 'text',
type: 'enum',
options: [
{ label: 'education', value: 'education' },
{ label: 'online_service', value: 'online_service' },
{ label: 'outbound', value: 'outbound' },
],
meaning: 'intelligentSemanticPruningSceneDesc',
},
// Intelligent semantic pruning阈值

View File

@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 17:30:02
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 17:30:02
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-06 13:50:05
*/
/**
* Memory Extraction Engine Configuration Page
@@ -13,7 +13,7 @@
import { type FC, useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom'
import { Row, Col, Space, Select, InputNumber, Slider, App, Form } from 'antd'
import { Row, Col, Space, Select, InputNumber, Slider, App, Form, Input } from 'antd'
import clsx from 'clsx'
import Card from './components/Card'
@@ -35,15 +35,15 @@ const keys = [
/**
* Configuration description component
*/
const ConfigDesc: FC<{ config: Variable, className?: string }> = ({config, className}) => {
const ConfigDesc: FC<{ config: Variable, className?: string; onlyMeaning?: boolean; }> = ({ config, className, onlyMeaning = false}) => {
const { t } = useTranslation();
return (
<div className={className}>
<Space size={8} className={clsx("rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4 ")}>
{!onlyMeaning && <Space size={8} className={clsx("rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4 ")}>
{config.variableName && <span className="rb:font-regular">{t('memoryExtractionEngine.variableName')}: {config.variableName}</span>}
{config.control && <span className="rb:font-regular">{t('memoryExtractionEngine.control')}: {t(`memoryExtractionEngine.${config.control}`)}</span>}
{config.type && <span className="rb:font-regular">{t('memoryExtractionEngine.type')}: {config.type}</span>}
</Space>
</Space>}
{config.meaning && <div className={clsx("rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4 ")}>{t('memoryExtractionEngine.Meaning')}: {t(`memoryExtractionEngine.${config.meaning}`)}</div>}
</div>
)
@@ -253,6 +253,21 @@ const MemoryExtractionEngine: FC = () => {
</div>
</>
}
{config.control === 'text' &&
<>
<div className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mt-6 rb:mb-2">
-{t(`memoryExtractionEngine.${config.label}`)}
</div>
<div className="rb:pl-2">
<Form.Item
name={config.variableName}
>
<Input placeholder={t('common.pleaseEnter')} disabled />
</Form.Item>
<ConfigDesc config={config} onlyMeaning={true} className="rb:-mt-4!" />
</div>
</>
}
</div>
))}
</div>

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 17:33:15
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-05 16:28:58
* @Last Modified time: 2026-03-06 13:53:53
*/
/**
* Memory Management Page
@@ -154,10 +154,10 @@ const MemoryManagement: React.FC = () => {
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
onClick={() => handleEdit(item)}
></div>
<div
{!item.is_system_default && <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')]"
onClick={() => handleDelete(item)}
></div>
></div>}
</Space>
</div>
</RbCard>

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:49:45
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-04 11:50:47
* @Last Modified time: 2026-03-06 12:26:12
*/
/**
* Model List Detail Drawer
@@ -153,7 +153,7 @@ const ModelListDetail = forwardRef<ModelListDetailRef, ModelListDetailProps>(({
<div className="rb:absolute rb:bottom-4 rb:left-6 rb:right-6">
<Row gutter={12}>
<Col span={12}>
<Button block onClick={() => handleEdit(item)}>{t('modelNew.modelConfiguration')}</Button>
{!item.model_id && <Button block onClick={() => handleEdit(item)}>{t('modelNew.modelConfiguration')}</Button>}
</Col>
<Col span={12}>
<Button type="primary" ghost block onClick={() => handleKeyConfig(item)}>{t('modelNew.keyConfig')}</Button>

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:50:18
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-04 11:39:20
* @Last Modified time: 2026-03-06 12:26:11
*/
/**
* Type definitions for Model Management
@@ -121,6 +121,7 @@ export interface ModelApiKey {
* Model list item data structure
*/
export interface ModelListItem {
model_id?: string;
/** Model name */
model_name?: string;
/** Associated model config IDs */

View File

@@ -105,7 +105,7 @@ const Detail: FC = () => {
<Tag color="warning">{t('common.default')}</Tag>
</Space>}
subTitle={<Tooltip title={data.scene_description}><div className="rb:h-4 rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">{data.scene_description}</div></Tooltip>}
extra={data.is_system_default ? undefined : (<Space>
extra={!data.is_system_default ? undefined : (<Space>
<Button type="primary" ghost className="rb:h-6! rb:px-2! rb:leading-5.5!" onClick={handleAdd}>+ {t('ontology.addClass')}</Button>
<Button className="rb:h-6! rb:px-2! rb:leading-5.5!" type="primary" onClick={handleExtract}>+ {t('ontology.extract')}</Button>
</Space>)}