From 10f7fbf222ba2f5bc3db5cb2e5189ec0f33cba21 Mon Sep 17 00:00:00 2001
From: YANG QIA <2013xile@gmail.com>
Date: Thu, 26 Feb 2026 17:33:21 +0800
Subject: [PATCH 1/3] refactor(ai): optimize the API request logic (#8714)
* refactor(ai): optimize the API request logic
* fix: build
* chore: optimize
* chore: optimize
* chore: optimize
* chore: optimize
* chore: optimize
* chore: optimize
* fix: useMemo
---
.../core/client/src/ai/tools-manager/index.ts | 1 -
.../client/__tests__/chatbox/model.test.ts | 20 +-
.../ai-employees/AIEmployeesProvider.tsx | 16 +-
.../ai-employees/ContextAwareTooltip.tsx | 11 +-
.../ai-employees/admin/EnableSwitch.tsx | 6 +-
.../ai-employees/admin/SkillSettings.tsx | 16 +-
.../src/client/ai-employees/admin/hooks.ts | 10 +-
.../ai-employees/ai-coding/AICodingButton.tsx | 249 +++++++++---------
.../ai-employees/chatbox/AIEmployeeSwitch.tsx | 87 +++---
.../ai-employees/chatbox/ChatButton.tsx | 42 +--
.../ai-employees/chatbox/Conversations.tsx | 11 +-
.../ai-employees/chatbox/MessageRenderer.tsx | 13 +-
.../ai-employees/chatbox/ModelSwitcher.tsx | 14 +-
.../ai-employees/chatbox/UserPrompt.tsx | 14 +-
.../chatbox/generative-ui/ToolCard.tsx | 16 +-
.../chatbox/generative-ui/ToolModal.tsx | 17 +-
.../chatbox/hooks/useChatBoxActions.ts | 20 +-
.../chatbox/hooks/useChatBoxEffect.tsx | 14 +-
.../chatbox/hooks/useChatMessageActions.ts | 8 +-
.../src/client/ai-employees/chatbox/model.ts | 10 +-
.../ai-employees/data-modeling/setup.tsx | 9 +-
.../flow/context/ai-employees-data.ts | 32 ---
.../client/ai-employees/flow/context/index.ts | 10 -
.../flow/models/AIEmployeeActionModel.tsx | 2 +-
.../flow/models/AIEmployeeShortcutModel.tsx | 15 +-
.../ai-employees/hooks/useAIEmployeesData.ts | 56 ----
.../ai-employees/shortcuts/ShortcutList.tsx | 12 +-
.../@nocobase/plugin-ai/src/client/index.tsx | 15 +-
.../src/client/llm-services/LLMServices.tsx | 22 +-
.../llm-services/LLMServicesRepository.ts | 61 -----
.../hooks/useLLMServiceCatalog.ts | 10 +-
.../src/client/llm-services/utils.ts | 2 +-
.../client/repositories/AIConfigRepository.ts | 213 +++++++++++++++
.../hooks/useAIConfigRepository.ts} | 6 +-
.../src/client/flow/components/DaraButton.tsx | 17 +-
35 files changed, 603 insertions(+), 474 deletions(-)
delete mode 100644 packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/flow/context/ai-employees-data.ts
delete mode 100644 packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/flow/context/index.ts
delete mode 100644 packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/hooks/useAIEmployeesData.ts
delete mode 100644 packages/plugins/@nocobase/plugin-ai/src/client/llm-services/LLMServicesRepository.ts
create mode 100644 packages/plugins/@nocobase/plugin-ai/src/client/repositories/AIConfigRepository.ts
rename packages/plugins/@nocobase/plugin-ai/src/client/{llm-services/hooks/useLLMServicesRepository.ts => repositories/hooks/useAIConfigRepository.ts} (64%)
diff --git a/packages/core/client/src/ai/tools-manager/index.ts b/packages/core/client/src/ai/tools-manager/index.ts
index 8409cc4b97c..dae61c95148 100644
--- a/packages/core/client/src/ai/tools-manager/index.ts
+++ b/packages/core/client/src/ai/tools-manager/index.ts
@@ -40,4 +40,3 @@ export class DefaultToolsManager implements ToolsManager {
}
export * from './types';
-export * from './hooks';
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/__tests__/chatbox/model.test.ts b/packages/plugins/@nocobase/plugin-ai/src/client/__tests__/chatbox/model.test.ts
index 4f7b5e40027..e243e5e269c 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/__tests__/chatbox/model.test.ts
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/__tests__/chatbox/model.test.ts
@@ -12,15 +12,14 @@ import { ensureModel } from '../../ai-employees/chatbox/model';
describe('chatbox model recovery', () => {
it('resolves model when current selection is missing (historical conversation case)', async () => {
- const llmServicesRepository = {
- services: [
+ const aiConfigRepository = {
+ getLLMServices: vi.fn(() => [
{
llmService: 'svc-openai',
llmServiceTitle: 'OpenAI',
enabledModels: [{ label: 'GPT-4o', value: 'gpt-4o' }],
},
- ],
- load: vi.fn(async () => undefined),
+ ]),
};
const api = {
storage: {
@@ -31,27 +30,26 @@ describe('chatbox model recovery', () => {
const result = await ensureModel({
api,
- llmServicesRepository: llmServicesRepository as any,
+ aiConfigRepository: aiConfigRepository as any,
username: 'orin',
currentOverride: null,
onResolved,
});
expect(result).toEqual({ llmService: 'svc-openai', model: 'gpt-4o' });
- expect(llmServicesRepository.load).toHaveBeenCalledTimes(1);
+ expect(aiConfigRepository.getLLMServices).toHaveBeenCalledTimes(1);
expect(onResolved).toHaveBeenCalledWith({ llmService: 'svc-openai', model: 'gpt-4o' });
});
it('keeps current model when it is still valid', async () => {
- const llmServicesRepository = {
- services: [
+ const aiConfigRepository = {
+ getLLMServices: vi.fn(() => [
{
llmService: 'svc-openai',
llmServiceTitle: 'OpenAI',
enabledModels: [{ label: 'GPT-4o', value: 'gpt-4o' }],
},
- ],
- load: vi.fn(async () => undefined),
+ ]),
};
const api = {
storage: {
@@ -63,7 +61,7 @@ describe('chatbox model recovery', () => {
const result = await ensureModel({
api,
- llmServicesRepository: llmServicesRepository as any,
+ aiConfigRepository: aiConfigRepository as any,
username: 'orin',
currentOverride,
onResolved,
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/AIEmployeesProvider.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/AIEmployeesProvider.tsx
index 1aa21c08133..69675030f41 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/AIEmployeesProvider.tsx
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/AIEmployeesProvider.tsx
@@ -12,16 +12,12 @@ import { AISelectionProvider } from './1.x/selector/AISelectorProvider';
import { AISettingsProvider } from './AISettingsProvider';
import { ChatBoxLayout } from './chatbox/ChatBoxLayout';
import { AISelection } from './AISelection';
-import { ContextAwareTooltip } from './ContextAwareTooltip';
import { AISelectionControl } from './AISelectionControl';
-import { CurrentUserContext, ToolsProvider, useApp } from '@nocobase/client';
+import { CurrentUserContext } from '@nocobase/client';
export const AIEmployeesProvider: React.FC<{
children: React.ReactNode;
}> = (props) => {
- const app = useApp();
- const { toolsManager } = app.aiManager;
-
const currentUserCtx = useContext(CurrentUserContext);
if (!currentUserCtx?.data?.data) {
return <>{props.children}>;
@@ -30,12 +26,10 @@ export const AIEmployeesProvider: React.FC<{
return (
-
- {props.children}
- {/* */}
-
-
-
+ {props.children}
+ {/* */}
+
+
);
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/ContextAwareTooltip.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/ContextAwareTooltip.tsx
index 4e746c44225..c5f9bfced90 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/ContextAwareTooltip.tsx
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/ContextAwareTooltip.tsx
@@ -13,12 +13,17 @@ import { useT } from '../locale';
import { Tooltip, Avatar, Flex } from 'antd';
import { contextAware } from './stores/context-aware';
import { avatars } from './avatars';
-import { useAIEmployeesData } from './hooks/useAIEmployeesData';
-import { useToken } from '@nocobase/client';
+import { useRequest, useToken } from '@nocobase/client';
+import { useAIConfigRepository } from '../repositories/hooks/useAIConfigRepository';
+import { AIEmployee } from './types';
export const ContextAwareTooltip: React.FC = observer(() => {
const t = useT();
- const { aiEmployeesMap } = useAIEmployeesData();
+ const aiConfigRepository = useAIConfigRepository();
+ useRequest(async () => {
+ return aiConfigRepository.getAIEmployees();
+ });
+ const aiEmployeesMap = aiConfigRepository.getAIEmployeesMap();
const { token } = useToken();
const [show, setShow] = useState(false);
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/admin/EnableSwitch.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/admin/EnableSwitch.tsx
index 97d838aef29..331c4ce504b 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/admin/EnableSwitch.tsx
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/admin/EnableSwitch.tsx
@@ -13,14 +13,14 @@ import { useField } from '@formily/react';
import { Field } from '@formily/core';
import { useCollectionRecordData, useDataBlockRequest, useDataBlockResource } from '@nocobase/client';
import { useT } from '../../locale';
-import { useAIEmployeesData } from '../hooks/useAIEmployeesData';
+import { useAIConfigRepository } from '../../repositories/hooks/useAIConfigRepository';
export const EnableSwitch: React.FC = () => {
const field = useField();
const record = useCollectionRecordData();
const resource = useDataBlockResource();
const { refresh } = useDataBlockRequest();
- const { refresh: refreshAIEmployees } = useAIEmployeesData();
+ const aiConfigRepository = useAIConfigRepository();
const { message } = App.useApp();
const t = useT();
const [loading, setLoading] = useState(false);
@@ -34,7 +34,7 @@ export const EnableSwitch: React.FC = () => {
});
message.success(t('Saved successfully'));
refresh();
- refreshAIEmployees();
+ await aiConfigRepository.refreshAIEmployees();
} catch (error) {
message.error(t('Failed to update'));
} finally {
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/admin/SkillSettings.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/admin/SkillSettings.tsx
index 1d7a56abf13..1d79555645d 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/admin/SkillSettings.tsx
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/admin/SkillSettings.tsx
@@ -11,9 +11,11 @@ import React, { useEffect, useRef, useState } from 'react';
import { List, Button, Dropdown, Tooltip, Space, Segmented, Flex, Collapse, Switch } from 'antd';
import { PlusOutlined, QuestionCircleOutlined, DeleteOutlined } from '@ant-design/icons';
import { useT } from '../../locale';
-import { SchemaComponent, useCollectionRecordData, useToken, useTools } from '@nocobase/client';
+import { SchemaComponent, useCollectionRecordData, useToken } from '@nocobase/client';
import { Schema, useField } from '@formily/react';
import { Field } from '@formily/core';
+import { useAIConfigRepository } from '../../repositories/hooks/useAIConfigRepository';
+import { observer } from '@nocobase/flow-engine';
export const SkillsListItem: React.FC<{
name: string;
@@ -53,14 +55,20 @@ export const SkillsListItem: React.FC<{
);
};
-export const Skills: React.FC = () => {
+export const Skills: React.FC = observer(() => {
const t = useT();
const { token } = useToken();
const field = useField();
- const { tools = [], loading } = useTools();
+ const aiConfigRepository = useAIConfigRepository();
+ const loading = aiConfigRepository.aiToolsLoading;
+ const tools = aiConfigRepository.aiTools;
const record = useCollectionRecordData();
const isBuiltIn = record?.builtIn;
+ useEffect(() => {
+ aiConfigRepository.getAITools();
+ }, [aiConfigRepository]);
+
const handleAdd = (name: string) => {
const skills = [...(field.value || [])];
if (!skills.some((s) => s.name === name)) {
@@ -365,7 +373,7 @@ export const Skills: React.FC = () => {
)}
>
);
-};
+});
export const SkillSettings: React.FC = () => {
const t = useT();
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/admin/hooks.ts b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/admin/hooks.ts
index 52c17816204..64adb863339 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/admin/hooks.ts
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/admin/hooks.ts
@@ -22,7 +22,7 @@ import { useT } from '../../locale';
import { useForm } from '@formily/react';
import { createForm } from '@formily/core';
import { uid } from '@formily/shared';
-import { useAIEmployeesData } from '../hooks/useAIEmployeesData';
+import { useAIConfigRepository } from '../../repositories/hooks/useAIConfigRepository';
export const useCreateFormProps = () => {
const t = useT();
@@ -80,7 +80,7 @@ export const useCreateActionProps = () => {
const form = useForm();
const api = useAPIClient();
const { refresh } = useDataBlockRequest();
- const { refresh: refreshAIEmployees } = useAIEmployeesData();
+ const aiConfigRepository = useAIConfigRepository();
const t = useT();
return {
@@ -95,7 +95,7 @@ export const useCreateActionProps = () => {
message.success(t('Saved successfully'));
setVisible(false);
form.reset();
- refreshAIEmployees();
+ await aiConfigRepository.refreshAIEmployees();
},
};
};
@@ -106,7 +106,7 @@ export const useEditActionProps = () => {
const form = useForm();
const resource = useDataBlockResource();
const { refresh } = useDataBlockRequest();
- const { refresh: refreshAIEmployees } = useAIEmployeesData();
+ const aiConfigRepository = useAIConfigRepository();
const collection = useCollection();
const filterTk = collection.getFilterTargetKey();
const t = useT();
@@ -134,7 +134,7 @@ export const useEditActionProps = () => {
message.success(t('Saved successfully'));
setVisible(false);
form.reset();
- refreshAIEmployees();
+ await aiConfigRepository.refreshAIEmployees();
},
};
};
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/ai-coding/AICodingButton.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/ai-coding/AICodingButton.tsx
index ad2a4365fe9..7af4b9c03eb 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/ai-coding/AICodingButton.tsx
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/ai-coding/AICodingButton.tsx
@@ -8,7 +8,7 @@
*/
import React, { useEffect, useState } from 'react';
-import { useAIEmployeesData } from '../hooks/useAIEmployeesData';
+import { useAIConfigRepository } from '../../repositories/hooks/useAIConfigRepository';
import { useChatBoxStore } from '../chatbox/stores/chat-box';
import { useChatBoxActions } from '../chatbox/hooks/useChatBoxActions';
import { Avatar, Popover, Tooltip } from 'antd';
@@ -16,7 +16,7 @@ import { useChatMessagesStore } from '../chatbox/stores/chat-messages';
import { ProfileCard } from '../ProfileCard';
import { avatars } from '../avatars';
import { EditorRef } from '@nocobase/client';
-import { useFlowContext } from '@nocobase/flow-engine';
+import { observer, useFlowContext } from '@nocobase/flow-engine';
import { isEngineer } from '../built-in/utils';
import { Task } from '../types';
import { useT } from '../../locale';
@@ -30,122 +30,60 @@ export interface AICodingButtonProps {
setActive: (key: string, active: boolean) => void;
}
-export const AICodingButton: React.FC = ({ uid, scene, language, editorRef, setActive }) => {
- const t = useT();
- const { aiEmployees } = useAIEmployeesData();
- const open = useChatBoxStore.use.open();
- const currentEmployee = useChatBoxStore.use.currentEmployee();
- const { triggerTask } = useChatBoxActions();
- const addContextItems = useChatMessagesStore.use.addContextItems();
- const setEditorRef = useChatMessagesStore.use.setEditorRef();
- const setCurrentEditorRefUid = useChatMessagesStore.use.setCurrentEditorRefUid();
- const ctx = useFlowContext();
+export const AICodingButton: React.FC = observer(
+ ({ uid, scene, language, editorRef, setActive }) => {
+ const t = useT();
+ const aiConfigRepository = useAIConfigRepository();
+ const aiEmployees = aiConfigRepository.aiEmployees;
+ const open = useChatBoxStore.use.open();
+ const currentEmployee = useChatBoxStore.use.currentEmployee();
+ const { triggerTask } = useChatBoxActions();
+ const addContextItems = useChatMessagesStore.use.addContextItems();
+ const setEditorRef = useChatMessagesStore.use.setEditorRef();
+ const setCurrentEditorRefUid = useChatMessagesStore.use.setCurrentEditorRefUid();
+ const ctx = useFlowContext();
- const aiEmployee = aiEmployees.filter((e) => isEngineer(e))[0];
+ const aiEmployee = aiEmployees.filter((e) => isEngineer(e))[0];
- useEffect(() => {
- setEditorRef(uid, editorRef);
- setCurrentEditorRefUid(uid);
- return () => {
- setEditorRef(uid, null);
- };
- }, [uid, editorRef, setEditorRef, setCurrentEditorRefUid]);
+ useEffect(() => {
+ aiConfigRepository.getAIEmployees();
+ }, [aiConfigRepository]);
- useEffect(() => {
- if (aiEmployee) {
- setActive('AICodingButton', true);
- } else {
- setActive('AICodingButton', false);
- }
- }, [aiEmployee, setActive]);
+ useEffect(() => {
+ setEditorRef(uid, editorRef);
+ setCurrentEditorRefUid(uid);
+ return () => {
+ setEditorRef(uid, null);
+ };
+ }, [uid, editorRef, setEditorRef, setCurrentEditorRefUid]);
- const [showTooltip, setShowTooltip] = useState(false);
- const [errorOccurred, setErrorOccurred] = useState(false);
- useEffect(() => {
- const isError = editorRef.logs.find((log) => log.level === 'error') !== undefined;
- setErrorOccurred(isError);
- setShowTooltip(isError);
- if (isError) {
- setTimeout(() => {
- setShowTooltip(false);
- }, 2000);
- }
- }, [editorRef.logs]);
+ useEffect(() => {
+ if (aiEmployee) {
+ setActive('AICodingButton', true);
+ } else {
+ setActive('AICodingButton', false);
+ }
+ }, [aiEmployee, setActive]);
- const TaskTemplate = (prototype: Partial) => {
- const { message, ...rest } = prototype;
- return {
- message: {
- workContext: [
- {
- type: 'code-editor',
- uid,
- title: `${scene}(${language})`,
- content: {
- scene,
- language,
- code: editorRef?.read(),
- },
- },
- ],
- ...(message ?? {}),
- },
- autoSend: false,
- ...rest,
- };
- };
+ const [showTooltip, setShowTooltip] = useState(false);
+ const [errorOccurred, setErrorOccurred] = useState(false);
+ useEffect(() => {
+ const isError = editorRef.logs.find((log) => log.level === 'error') !== undefined;
+ setErrorOccurred(isError);
+ setShowTooltip(isError);
+ if (isError) {
+ setTimeout(() => {
+ setShowTooltip(false);
+ }, 2000);
+ }
+ }, [editorRef.logs]);
- const taskMap: Record = {
- generateCode: TaskTemplate({ title: t('Start coding') }),
- codeReview: TaskTemplate({
- title: t('Code review'),
- message: { user: t('please review the code'), system: prompts.codeReview },
- }),
- logsDiagnosis: TaskTemplate({
- title: t('Diagnose and fix the error'),
- message: {
- user: t('please fix the error'),
- system: `Here is run logs:${JSON.stringify(
- editorRef?.logs,
- )} \n analyze the code and run logs, then fix the problems existing in the code`,
- },
- autoSend: errorOccurred,
- }),
- };
-
- const tasks: Task[] = Object.values(taskMap);
-
- // Store flow context for frontend context tools
- useChatMessagesStore.getState().setFlowContext(ctx);
-
- return aiEmployee ? (
-
- }>
- {
- if (!open || currentEmployee?.username !== aiEmployee.username) {
- if (editorRef.logs.find((log) => log.level === 'error')) {
- triggerTask({ aiEmployee, tasks: [taskMap['logsDiagnosis']] });
- } else {
- triggerTask({ aiEmployee, tasks });
- }
- }
-
- setCurrentEditorRefUid(uid);
-
- addContextItems({
+ const TaskTemplate = (prototype: Partial) => {
+ const { message, ...rest } = prototype;
+ return {
+ message: {
+ workContext: [
+ {
type: 'code-editor',
uid,
title: `${scene}(${language})`,
@@ -154,12 +92,81 @@ export const AICodingButton: React.FC = ({ uid, scene, lang
language,
code: editorRef?.read(),
},
- });
- }}
- />
-
-
- ) : (
- <>>
- );
-};
+ },
+ ],
+ ...(message ?? {}),
+ },
+ autoSend: false,
+ ...rest,
+ };
+ };
+
+ const taskMap: Record = {
+ generateCode: TaskTemplate({ title: t('Start coding') }),
+ codeReview: TaskTemplate({
+ title: t('Code review'),
+ message: { user: t('please review the code'), system: prompts.codeReview },
+ }),
+ logsDiagnosis: TaskTemplate({
+ title: t('Diagnose and fix the error'),
+ message: {
+ user: t('please fix the error'),
+ system: `Here is run logs:${JSON.stringify(
+ editorRef?.logs,
+ )} \n analyze the code and run logs, then fix the problems existing in the code`,
+ },
+ autoSend: errorOccurred,
+ }),
+ };
+
+ const tasks: Task[] = Object.values(taskMap);
+
+ // Store flow context for frontend context tools
+ useChatMessagesStore.getState().setFlowContext(ctx);
+
+ return aiEmployee ? (
+
+ }>
+ {
+ if (!open || currentEmployee?.username !== aiEmployee.username) {
+ if (editorRef.logs.find((log) => log.level === 'error')) {
+ triggerTask({ aiEmployee, tasks: [taskMap['logsDiagnosis']] });
+ } else {
+ triggerTask({ aiEmployee, tasks });
+ }
+ }
+
+ setCurrentEditorRefUid(uid);
+
+ addContextItems({
+ type: 'code-editor',
+ uid,
+ title: `${scene}(${language})`,
+ content: {
+ scene,
+ language,
+ code: editorRef?.read(),
+ },
+ });
+ }}
+ />
+
+
+ ) : (
+ <>>
+ );
+ },
+);
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/AIEmployeeSwitch.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/AIEmployeeSwitch.tsx
index 6865ef019f3..445832c6885 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/AIEmployeeSwitch.tsx
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/AIEmployeeSwitch.tsx
@@ -7,10 +7,11 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-import React, { useMemo, useState } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
import { Avatar, Button, Divider, Dropdown, Flex, Popover, Tag } from 'antd';
import { UserAddOutlined, CloseCircleOutlined, CheckOutlined, DownOutlined } from '@ant-design/icons';
import { useToken } from '@nocobase/client';
+import { observer } from '@nocobase/flow-engine';
import { useT } from '../../locale';
import { AIEmployeeListItem } from '../AIEmployeeListItem';
import { avatars } from '../avatars';
@@ -20,19 +21,23 @@ import { ContextItemsHeader } from './ContextItemsHeader';
import { useChatBoxStore } from './stores/chat-box';
import { useChatBoxActions } from './hooks/useChatBoxActions';
import { EditMessageHeader } from './EditMessageHeader';
-import { useAIEmployeesData } from '../hooks/useAIEmployeesData';
+import { useAIConfigRepository } from '../../repositories/hooks/useAIConfigRepository';
-export const AIEmployeeSwitcher: React.FC = () => {
+export const AIEmployeeSwitcher: React.FC = observer(() => {
const t = useT();
const [isOpen, setIsOpen] = useState(false);
- const { aiEmployees } = useAIEmployeesData();
+ const aiConfigRepository = useAIConfigRepository();
+ const aiEmployees = aiConfigRepository.aiEmployees;
const currentEmployee = useChatBoxStore.use.currentEmployee();
const { switchAIEmployee } = useChatBoxActions();
const { token } = useToken();
- const menuItems = useMemo(() => {
- if (!aiEmployees.length) {
- return [
+ useEffect(() => {
+ aiConfigRepository.getAIEmployees();
+ }, [aiConfigRepository]);
+
+ const menuItems = !aiEmployees.length
+ ? [
{
key: 'empty',
label: (
@@ -41,23 +46,20 @@ export const AIEmployeeSwitcher: React.FC = () => {
disabled: true,
style: { cursor: 'default', padding: '16px 12px', height: 'auto', minHeight: 0 },
},
- ];
- }
-
- return aiEmployees.map((employee) => {
- const isSelected = currentEmployee?.username === employee.username;
- return {
- key: employee.username,
- label: (
-
-
- {isSelected && }
-
- ),
- onClick: () => switchAIEmployee(employee),
- };
- });
- }, [aiEmployees, currentEmployee?.username, switchAIEmployee, t, token.colorPrimary, token.colorTextSecondary]);
+ ]
+ : aiEmployees.map((employee) => {
+ const isSelected = currentEmployee?.username === employee.username;
+ return {
+ key: employee.username,
+ label: (
+
+
+ {isSelected && }
+
+ ),
+ onClick: () => switchAIEmployee(employee),
+ };
+ });
const hasEmployees = aiEmployees.length > 0;
const currentLabel = currentEmployee ? currentEmployee.nickname : `${t('Select an')} ${t('AI employee')}`;
@@ -105,10 +107,11 @@ export const AIEmployeeSwitcher: React.FC = () => {
{dropdownContent}
);
-};
+});
-export const SenderHeader: React.FC = () => {
- const { aiEmployees } = useAIEmployeesData();
+export const SenderHeader: React.FC = observer(() => {
+ const aiConfigRepository = useAIConfigRepository();
+ const aiEmployees = aiConfigRepository.aiEmployees;
const { token } = useToken();
const t = useT();
@@ -117,19 +120,21 @@ export const SenderHeader: React.FC = () => {
const { switchAIEmployee } = useChatBoxActions();
- const items = useMemo(() => {
- return aiEmployees?.map((employee) => ({
- key: employee.username,
- label: (
- {
- switchAIEmployee(employee);
- }}
- />
- ),
- }));
- }, [aiEmployees, switchAIEmployee]);
+ useEffect(() => {
+ aiConfigRepository.getAIEmployees();
+ }, [aiConfigRepository]);
+
+ const items = aiEmployees?.map((employee) => ({
+ key: employee.username,
+ label: (
+ {
+ switchAIEmployee(employee);
+ }}
+ />
+ ),
+ }));
const avatar = useMemo(() => {
if (!currentEmployee) {
@@ -222,4 +227,4 @@ export const SenderHeader: React.FC = () => {
{currentEmployee ? : null}
);
-};
+});
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/ChatButton.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/ChatButton.tsx
index 31e4c496227..5d7ff903645 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/ChatButton.tsx
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/ChatButton.tsx
@@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-import React, { useMemo, useState } from 'react';
+import React, { useState } from 'react';
import { Avatar, Dropdown, FloatButton } from 'antd';
import icon from '../icon.svg';
import { css } from '@emotion/css';
@@ -15,7 +15,7 @@ import { AIEmployeeListItem } from '../AIEmployeeListItem';
import { useMobileLayout, useToken } from '@nocobase/client';
import { useChatBoxStore } from './stores/chat-box';
import { useChatBoxActions } from './hooks/useChatBoxActions';
-import { useAIEmployeesData } from '../hooks/useAIEmployeesData';
+import { useAIConfigRepository } from '../../repositories/hooks/useAIConfigRepository';
import { FlowRuntimeContext, observer, useFlowContext } from '@nocobase/flow-engine';
import { isHide } from '../built-in/utils';
import { useChatConversationsStore } from './stores/chat-conversations';
@@ -26,7 +26,11 @@ export const ChatButton: React.FC = observer(() => {
const isV1Page = ctx?.pageInfo?.version === 'v1';
const { token } = useToken();
- const { aiEmployees } = useAIEmployeesData();
+ const aiConfigRepository = useAIConfigRepository();
+ const aiEmployees = aiConfigRepository.aiEmployees;
+ React.useEffect(() => {
+ aiConfigRepository.getAIEmployees();
+ }, [aiConfigRepository]);
const [dropdownOpen, setDropdownOpen] = useState(false);
@@ -38,23 +42,21 @@ export const ChatButton: React.FC = observer(() => {
const setWebSearch = useChatConversationsStore.use.setWebSearch();
- const items = useMemo(() => {
- return aiEmployees
- ?.filter((employee) => !isHide(employee))
- .map((employee) => ({
- key: employee.username,
- label: (
- {
- setWebSearch(true);
- setOpen(true);
- switchAIEmployee(employee);
- }}
- />
- ),
- }));
- }, [aiEmployees]);
+ const items = aiEmployees
+ ?.filter((employee) => !isHide(employee))
+ .map((employee) => ({
+ key: employee.username,
+ label: (
+ {
+ setWebSearch(true);
+ setOpen(true);
+ switchAIEmployee(employee);
+ }}
+ />
+ ),
+ }));
if (open || !aiEmployees?.length || isV1Page) {
return null;
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/Conversations.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/Conversations.tsx
index c69bd9ae4c2..64c40649811 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/Conversations.tsx
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/Conversations.tsx
@@ -10,7 +10,7 @@
import React, { memo, useEffect, useMemo, useRef } from 'react';
import { Input, Empty, Spin, App } from 'antd';
import { Conversations as AntConversations } from '@ant-design/x';
-import { SchemaComponent, useAPIClient, useActionContext } from '@nocobase/client';
+import { SchemaComponent, useAPIClient, useActionContext, useRequest } from '@nocobase/client';
import { css } from '@emotion/css';
import { useT } from '../../locale';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
@@ -23,7 +23,8 @@ import { useChatMessagesStore } from './stores/chat-messages';
import { useChatMessageActions } from './hooks/useChatMessageActions';
import { useChatBoxActions } from './hooks/useChatBoxActions';
import { useChatBoxStore } from './stores/chat-box';
-import { useAIEmployeesData } from '../hooks/useAIEmployeesData';
+import { useAIConfigRepository } from '../../repositories/hooks/useAIConfigRepository';
+import { AIEmployee } from '../types';
const useCloseActionProps = () => {
const { setVisible } = useActionContext();
@@ -131,7 +132,11 @@ export const Conversations: React.FC = memo(() => {
const t = useT();
const api = useAPIClient();
const { modal, message } = App.useApp();
- const { aiEmployeesMap } = useAIEmployeesData();
+ const aiConfigRepository = useAIConfigRepository();
+ useRequest(async () => {
+ return aiConfigRepository.getAIEmployees();
+ });
+ const aiEmployeesMap = aiConfigRepository.getAIEmployeesMap();
const currentEmployee = useChatBoxStore.use.currentEmployee();
const setCurrentEmployee = useChatBoxStore.use.setCurrentEmployee();
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/MessageRenderer.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/MessageRenderer.tsx
index ba62d63e78f..b8a9e2667c8 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/MessageRenderer.tsx
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/MessageRenderer.tsx
@@ -12,7 +12,7 @@ import { Button, Space, App, Alert, Flex, Collapse, Typography, Tooltip } from '
import { CopyOutlined, ReloadOutlined, EditOutlined, ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
import { Attachments, Bubble } from '@ant-design/x';
import { useT } from '../../locale';
-import { lazy, usePlugin, useToken, useTools, toToolsMap } from '@nocobase/client';
+import { lazy, usePlugin, useToken, toToolsMap } from '@nocobase/client';
import PluginAIClient from '../..';
import { cx, css } from '@emotion/css';
import { Message, Task } from '../types';
@@ -24,6 +24,8 @@ import { useChatBoxStore } from './stores/chat-box';
import { useChatMessagesStore } from './stores/chat-messages';
import { useChatBoxActions } from './hooks/useChatBoxActions';
import _ from 'lodash';
+import { useAIConfigRepository } from '../../repositories/hooks/useAIConfigRepository';
+import { observer } from '@nocobase/flow-engine';
const { Markdown } = lazy(() => import('./markdown/Markdown'), 'Markdown');
@@ -106,12 +108,17 @@ const AIMessageRenderer: React.FC<{
export const AIMessage: React.FC<{
msg: Message['content'];
-}> = memo(({ msg }) => {
+}> = observer(({ msg }) => {
const t = useT();
const { token } = useToken();
const { message } = App.useApp();
- const { tools, loading: toolsLoading } = useTools();
+ const aiConfigRepository = useAIConfigRepository();
+ const toolsLoading = aiConfigRepository.aiToolsLoading;
+ const tools = aiConfigRepository.aiTools;
const toolsMap = useMemo(() => toToolsMap(tools || []), [tools]);
+ useEffect(() => {
+ aiConfigRepository.getAITools();
+ }, [aiConfigRepository]);
const plugin = usePlugin('ai') as PluginAIClient;
const provider = plugin.aiManager.llmProviders.get(msg.metadata?.provider);
const hasCustomRenderer = !!provider?.components?.MessageRenderer;
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/ModelSwitcher.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/ModelSwitcher.tsx
index ee7214d5237..f29f235d48f 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/ModelSwitcher.tsx
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/ModelSwitcher.tsx
@@ -37,6 +37,10 @@ export const ModelSwitcher: React.FC = observer(
const { repo, services: llmServices, loading, allModelsWithLabel, allModels } = useLLMServiceCatalog();
+ const servicesWithModels = llmServices.filter(
+ (service) => Array.isArray(service.enabledModels) && service.enabledModels.length > 0,
+ );
+
// Initialize: cache >> first model
useEffect(() => {
if (!currentEmployeeUsername || !allModels.length) return;
@@ -101,12 +105,12 @@ export const ModelSwitcher: React.FC = observer(
if (!currentEmployee) return null;
if (loading && !llmServices.length) return ;
- const hasModels = allModels.length > 0;
+ const hasModels = servicesWithModels.length > 0;
// Build dropdown menu items
const menuItems: any[] = [];
- llmServices.forEach((service, sIndex) => {
+ servicesWithModels.forEach((service, sIndex) => {
if (sIndex > 0) {
menuItems.push({ type: 'divider', key: `divider-${sIndex}` });
}
@@ -211,7 +215,11 @@ export const ModelSwitcher: React.FC = observer(
{dropdownContent}
{hasConfigPermission && (
- setAddModalOpen(false)} onSuccess={() => repo.refresh()} />
+ setAddModalOpen(false)}
+ onSuccess={() => repo.refreshLLMServices()}
+ />
)}
>
);
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/UserPrompt.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/UserPrompt.tsx
index e8ed9e4a514..8913e8d4a4e 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/UserPrompt.tsx
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/UserPrompt.tsx
@@ -8,14 +8,15 @@
*/
import React, { useMemo } from 'react';
-import { SchemaComponent, useAPIClient, useActionContext, useToken } from '@nocobase/client';
+import { SchemaComponent, useAPIClient, useActionContext, useRequest, useToken } from '@nocobase/client';
import { useT } from '../../locale';
import { Button, Popover, Card, Alert, App, Typography } from 'antd';
import { InfoCircleOutlined } from '@ant-design/icons';
import { useForm } from '@formily/react';
import { uid } from '@formily/shared';
import { useChatBoxStore } from './stores/chat-box';
-import { useAIEmployeesData } from '../hooks/useAIEmployeesData';
+import { useAIConfigRepository } from '../../repositories/hooks/useAIConfigRepository';
+import { AIEmployee } from '../types';
const useCancelActionProps = () => {
const { setVisible } = useActionContext();
@@ -38,8 +39,7 @@ const useEditActionProps = () => {
const currentEmployee = useChatBoxStore.use.currentEmployee();
const setCurrentEmployee = useChatBoxStore.use.setCurrentEmployee();
-
- const { refresh } = useAIEmployeesData();
+ const aiConfigRepository = useAIConfigRepository();
return {
type: 'primary',
@@ -51,7 +51,7 @@ const useEditActionProps = () => {
prompt: form.values?.prompt,
},
});
- refresh();
+ await aiConfigRepository.refreshAIEmployees();
setCurrentEmployee((prev) => ({
...prev,
userConfig: {
@@ -134,6 +134,10 @@ const Edit: React.FC = () => {
export const UserPrompt: React.FC = () => {
const t = useT();
const { token } = useToken();
+ const aiConfigRepository = useAIConfigRepository();
+ useRequest(async () => {
+ return aiConfigRepository.getAIEmployees();
+ });
const currentEmployee = useChatBoxStore.use.currentEmployee();
return (
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/generative-ui/ToolCard.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/generative-ui/ToolCard.tsx
index ffd7af83df7..c58609587a3 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/generative-ui/ToolCard.tsx
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/generative-ui/ToolCard.tsx
@@ -9,17 +9,21 @@
import React, { ComponentType, useEffect } from 'react';
import { DefaultToolCard } from './DefaultToolCard';
-import { ToolsUIProperties, toToolsMap, useTools } from '@nocobase/client';
+import { ToolsUIProperties, toToolsMap } from '@nocobase/client';
import { ToolCall } from '../../types';
import { jsonrepair } from 'jsonrepair';
import { useToolCallActions } from '../hooks/useToolCallActions';
+import { useAIConfigRepository } from '../../../repositories/hooks/useAIConfigRepository';
+import { observer } from '@nocobase/flow-engine';
export const ToolCard: React.FC<{
messageId: string;
toolCalls: ToolCall[];
inlineActions?: React.ReactNode;
-}> = ({ toolCalls, messageId, inlineActions }) => {
- const { tools, loading } = useTools();
+}> = observer(({ toolCalls, messageId, inlineActions }) => {
+ const aiConfigRepository = useAIConfigRepository();
+ const loading = aiConfigRepository.aiToolsLoading;
+ const tools = aiConfigRepository.aiTools;
const toolsMap = toToolsMap(tools);
const { getDecisionActions } = useToolCallActions({ messageId });
const toolsWithUI: ({ C: ComponentType } & ToolsUIProperties)[] = [];
@@ -55,6 +59,10 @@ export const ToolCard: React.FC<{
}
}
+ useEffect(() => {
+ aiConfigRepository.getAITools();
+ }, [aiConfigRepository]);
+
useEffect(() => {
if (!messageId) {
return;
@@ -94,4 +102,4 @@ export const ToolCard: React.FC<{
)}
>
);
-};
+});
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/generative-ui/ToolModal.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/generative-ui/ToolModal.tsx
index 1cf0b7b479c..9f00bd6b624 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/generative-ui/ToolModal.tsx
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/generative-ui/ToolModal.tsx
@@ -7,15 +7,17 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-import React, { useCallback } from 'react';
+import React, { useCallback, useEffect } from 'react';
import { Modal, Select, message } from 'antd';
import { useChatToolsStore } from '../stores/chat-tools';
-import { ToolsUIProperties, toToolsMap, useTools } from '@nocobase/client';
+import { ToolsUIProperties, toToolsMap } from '@nocobase/client';
import { Schema } from '@formily/react';
+import { observer } from '@nocobase/flow-engine';
import { useT } from '../../../locale';
import { useChatMessageActions } from '../hooks/useChatMessageActions';
import { useChatConversationsStore } from '../stores/chat-conversations';
import { useToolCallActions } from '../hooks/useToolCallActions';
+import { useAIConfigRepository } from '../../../repositories/hooks/useAIConfigRepository';
const useDefaultOnOk = (decisions: ToolsUIProperties['decisions']) => {
return {
@@ -23,11 +25,16 @@ const useDefaultOnOk = (decisions: ToolsUIProperties['decisions']) => {
};
};
-export const ToolModal: React.FC = () => {
+export const ToolModal: React.FC = observer(() => {
const t = useT();
- const { tools } = useTools();
+ const aiConfigRepository = useAIConfigRepository();
+ const tools = aiConfigRepository.aiTools;
const toolsMap = toToolsMap(tools);
+ useEffect(() => {
+ aiConfigRepository.getAITools();
+ }, [aiConfigRepository]);
+
const open = useChatToolsStore.use.openToolModal();
const setOpen = useChatToolsStore.use.setOpenToolModal();
const activeTool = useChatToolsStore.use.activeTool();
@@ -126,4 +133,4 @@ export const ToolModal: React.FC = () => {
{C ? : null}
);
-};
+});
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/hooks/useChatBoxActions.ts b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/hooks/useChatBoxActions.ts
index 9423deb22ba..1741893a932 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/hooks/useChatBoxActions.ts
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/hooks/useChatBoxActions.ts
@@ -20,12 +20,12 @@ import { uid } from '@formily/shared';
import { aiEmployeeRole } from '../roles';
import { useChatToolsStore } from '../stores/chat-tools';
import { useAPIClient } from '@nocobase/client';
-import { useLLMServicesRepository } from '../../../llm-services/hooks/useLLMServicesRepository';
+import { useAIConfigRepository } from '../../../repositories/hooks/useAIConfigRepository';
import { getAllModels, isSameModel, isValidModel, resolveModel } from '../model';
export const useChatBoxActions = () => {
const api = useAPIClient();
- const llmServicesRepository = useLLMServicesRepository();
+ const aiConfigRepository = useAIConfigRepository();
const t = useT();
const open = useChatBoxStore.use.open();
@@ -91,8 +91,7 @@ export const useChatBoxActions = () => {
const ensureModel = useCallback(
async (aiEmployee: AIEmployee) => {
- await llmServicesRepository.load();
- const allModels = getAllModels(llmServicesRepository.services);
+ const allModels = getAllModels(await aiConfigRepository.getLLMServices());
const currentModel = useChatBoxStore.getState().model;
const resolvedModel = resolveModel(api, aiEmployee.username, allModels, currentModel);
if (!isSameModel(currentModel, resolvedModel)) {
@@ -100,13 +99,12 @@ export const useChatBoxActions = () => {
}
return resolvedModel;
},
- [api, llmServicesRepository, setModel],
+ [api, aiConfigRepository, setModel],
);
const resolveTaskModel = useCallback(
async (aiEmployee: AIEmployee, taskModel?: { llmService: string; model: string } | null) => {
- await llmServicesRepository.load();
- const allModels = getAllModels(llmServicesRepository.services);
+ const allModels = getAllModels(await aiConfigRepository.getLLMServices());
if (isValidModel(taskModel, allModels)) {
const currentModel = useChatBoxStore.getState().model;
if (!isSameModel(currentModel, taskModel)) {
@@ -121,7 +119,7 @@ export const useChatBoxActions = () => {
}
return resolvedModel;
},
- [api, llmServicesRepository, setModel],
+ [api, aiConfigRepository, setModel],
);
const startNewConversation = useCallback(() => {
@@ -205,7 +203,9 @@ export const useChatBoxActions = () => {
model: taskModel,
} = await parseTask(task);
const resolvedModel = await resolveTaskModel(aiEmployee, taskModel);
- const service = llmServicesRepository.services.find((s) => s.llmService === resolvedModel?.llmService);
+ const service = (await aiConfigRepository.getLLMServices()).find(
+ (s) => s.llmService === resolvedModel?.llmService,
+ );
const resolvedWebSearch =
service?.supportWebSearch === false ? false : typeof webSearch === 'boolean' ? webSearch : false;
setWebSearch(resolvedWebSearch);
@@ -249,7 +249,7 @@ export const useChatBoxActions = () => {
});
setMessages(msgs);
},
- [open, currentConversation, ensureModel, llmServicesRepository, resolveTaskModel, setWebSearch],
+ [open, currentConversation, ensureModel, aiConfigRepository, resolveTaskModel, setWebSearch],
);
return {
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/hooks/useChatBoxEffect.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/hooks/useChatBoxEffect.tsx
index be3d1060959..43a2e3430aa 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/hooks/useChatBoxEffect.tsx
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/hooks/useChatBoxEffect.tsx
@@ -7,15 +7,15 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-import { useContext, useEffect } from 'react';
+import { useEffect } from 'react';
import { useChatBoxStore } from '../stores/chat-box';
import { aiEmployeeRole, defaultRoles } from '../roles';
import { useChatConversationActions } from './useChatConversationActions';
-import { useAIEmployeesData } from '../../hooks/useAIEmployeesData';
-import { useTools } from '@nocobase/client';
+import { useAIConfigRepository } from '../../../repositories/hooks/useAIConfigRepository';
export const useChatBoxEffect = () => {
- const { aiEmployees } = useAIEmployeesData();
+ const aiConfigRepository = useAIConfigRepository();
+ const aiEmployees = aiConfigRepository.aiEmployees;
const open = useChatBoxStore.use.open();
const senderRef = useChatBoxStore.use.senderRef();
@@ -24,7 +24,9 @@ export const useChatBoxEffect = () => {
const { conversationsService } = useChatConversationActions();
- const { refresh } = useTools();
+ useEffect(() => {
+ aiConfigRepository.getAIEmployees();
+ }, [aiConfigRepository]);
useEffect(() => {
if (!aiEmployees) {
@@ -55,7 +57,7 @@ export const useChatBoxEffect = () => {
if (open) {
conversationsService.run();
senderRef?.current?.focus();
- refresh();
+ aiConfigRepository.refreshAITools();
}
}, [open]);
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/hooks/useChatMessageActions.ts b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/hooks/useChatMessageActions.ts
index 675010b0469..f8b90b94ab1 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/hooks/useChatMessageActions.ts
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/hooks/useChatMessageActions.ts
@@ -20,7 +20,7 @@ import { useChatBoxStore } from '../stores/chat-box';
import { parseWorkContext } from '../utils';
import { aiDebugLogger } from '../../../debug-logger'; // [AI_DEBUG]
import { useChatToolCallStore } from '../stores/chat-tool-call';
-import { useLLMServicesRepository } from '../../../llm-services/hooks/useLLMServicesRepository';
+import { useAIConfigRepository } from '../../../repositories/hooks/useAIConfigRepository';
import { ensureModel } from '../model';
export const useChatMessageActions = () => {
@@ -28,7 +28,7 @@ export const useChatMessageActions = () => {
const t = useT();
const api = useAPIClient();
const plugin = usePlugin('ai') as PluginAIClient;
- const llmServicesRepository = useLLMServicesRepository();
+ const aiConfigRepository = useAIConfigRepository();
const setIsEditingMessage = useChatBoxStore.use.setIsEditingMessage();
const setEditingMessageId = useChatBoxStore.use.setEditingMessageId();
@@ -59,13 +59,13 @@ export const useChatMessageActions = () => {
}
return ensureModel({
api,
- llmServicesRepository,
+ aiConfigRepository,
username: targetUsername,
currentOverride: state.model,
onResolved: setModel,
});
},
- [api, llmServicesRepository, setModel],
+ [api, aiConfigRepository, setModel],
);
const messagesService = useRequest<{
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/model.ts b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/model.ts
index 9c456a0490c..36135030612 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/model.ts
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/model.ts
@@ -7,8 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-import { LLMServiceItem } from '../../llm-services/LLMServicesRepository';
-import type { LLMServicesRepository } from '../../llm-services/LLMServicesRepository';
+import { AIConfigRepository, LLMServiceItem } from '../../repositories/AIConfigRepository';
import { ModelRef } from './stores/chat-box';
export const MODEL_PREFERENCE_STORAGE_KEY = 'ai_model_preference_';
@@ -56,19 +55,18 @@ export const resolveModel = (api: any, username: string, allModels: ModelRef[],
export const ensureModel = async ({
api,
- llmServicesRepository,
+ aiConfigRepository,
username,
currentOverride,
onResolved,
}: {
api: any;
- llmServicesRepository: LLMServicesRepository;
+ aiConfigRepository: AIConfigRepository;
username: string;
currentOverride?: ModelRef | null;
onResolved?: (override: ModelRef | null) => void;
}): Promise => {
- await llmServicesRepository.load();
- const allModels = getAllModels(llmServicesRepository.services);
+ const allModels = getAllModels(await aiConfigRepository.getLLMServices());
const resolvedOverride = resolveModel(api, username, allModels, currentOverride);
if (!isSameModel(currentOverride, resolvedOverride)) {
onResolved?.(resolvedOverride);
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/data-modeling/setup.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/data-modeling/setup.tsx
index 54c742136f8..7db0aecda5b 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/data-modeling/setup.tsx
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/data-modeling/setup.tsx
@@ -13,10 +13,12 @@ import React, { useMemo, useState } from 'react';
import { Avatar, Button, Popover } from 'antd';
import { ProfileCard } from '../ProfileCard';
import { avatars } from '../avatars';
-import { useAIEmployeesData } from '../hooks/useAIEmployeesData';
+import { useAIConfigRepository } from '../../repositories/hooks/useAIConfigRepository';
import { useChatBoxStore } from '../chatbox/stores/chat-box';
import { useChatBoxActions } from '../chatbox/hooks/useChatBoxActions';
import { isDataModelingAssistant } from '../built-in/utils';
+import { useRequest } from '@nocobase/client';
+import { AIEmployee } from '../types';
export const setupDataModeling = (plugin: PluginAIClient) => {
const dataSourceManager = plugin.pm.get('data-source-manager');
@@ -27,7 +29,10 @@ export const setupDataModeling = (plugin: PluginAIClient) => {
const AIButton = () => {
const [focus, setFocus] = useState(false);
- const { aiEmployees } = useAIEmployeesData();
+ const aiConfigRepository = useAIConfigRepository();
+ const { data: aiEmployees = [] } = useRequest(async () => {
+ return aiConfigRepository.getAIEmployees();
+ });
const open = useChatBoxStore.use.open();
const setOpen = useChatBoxStore.use.setOpen();
const currentEmployee = useChatBoxStore.use.currentEmployee();
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/flow/context/ai-employees-data.ts b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/flow/context/ai-employees-data.ts
deleted file mode 100644
index c7589ce5fc9..00000000000
--- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/flow/context/ai-employees-data.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * This file is part of the NocoBase (R) project.
- * Copyright (c) 2020-2024 NocoBase Co., Ltd.
- * Authors: NocoBase Team.
- *
- * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
- * For more information, please refer to: https://www.nocobase.com/agreement.
- */
-
-import { FlowEngineContext, PropertyOptions } from '@nocobase/flow-engine';
-import { AIEmployee } from '../../types';
-
-export const aiEmployeesData: [string, PropertyOptions] = [
- 'aiEmployeesData',
- {
- get: async (ctx: FlowEngineContext) => {
- const aiEmployees: AIEmployee[] = await ctx.api
- .resource('aiEmployees')
- .listByUser()
- .then((res) => res?.data?.data);
-
- const aiEmployeesMap: {
- [username: string]: AIEmployee;
- } = (aiEmployees || []).reduce((acc, aiEmployee) => {
- acc[aiEmployee.username] = aiEmployee;
- return acc;
- }, {});
-
- return { aiEmployees, aiEmployeesMap };
- },
- },
-];
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/flow/context/index.ts b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/flow/context/index.ts
deleted file mode 100644
index 6711e2ab2c9..00000000000
--- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/flow/context/index.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
- * This file is part of the NocoBase (R) project.
- * Copyright (c) 2020-2024 NocoBase Co., Ltd.
- * Authors: NocoBase Team.
- *
- * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
- * For more information, please refer to: https://www.nocobase.com/agreement.
- */
-
-export * from './ai-employees-data';
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/flow/models/AIEmployeeActionModel.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/flow/models/AIEmployeeActionModel.tsx
index e9fa738ac1a..30b78d05c77 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/flow/models/AIEmployeeActionModel.tsx
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/flow/models/AIEmployeeActionModel.tsx
@@ -38,7 +38,7 @@ export class AIEmployeeActionModel extends ActionModel {
static scene = ActionSceneEnum.all;
static async defineChildren(ctx: FlowModelContext) {
- const { aiEmployees } = ctx.aiEmployeesData;
+ const aiEmployees = await ctx.aiConfigRepository.getAIEmployees();
return aiEmployees
?.filter((aiEmployee: AIEmployee) => !isHide(aiEmployee))
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/flow/models/AIEmployeeShortcutModel.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/flow/models/AIEmployeeShortcutModel.tsx
index e5dd2353ef1..f583697b269 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/flow/models/AIEmployeeShortcutModel.tsx
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/flow/models/AIEmployeeShortcutModel.tsx
@@ -14,8 +14,7 @@ import { avatars } from '../../avatars';
import { AIEmployee, TriggerTaskOptions, ContextItem as ContextItemType } from '../../types';
import { useChatBoxActions } from '../../chatbox/hooks/useChatBoxActions';
import { ProfileCard } from '../../ProfileCard';
-import { RemoteSelect, TextAreaWithContextSelector, useToken } from '@nocobase/client';
-import { useAIEmployeesData } from '../../hooks/useAIEmployeesData';
+import { RemoteSelect, TextAreaWithContextSelector, useRequest, useToken } from '@nocobase/client';
import { AddContextButton } from '../../AddContextButton';
import { Schema, useField } from '@formily/react';
import { ArrayField, ObjectField, Field } from '@formily/core';
@@ -29,6 +28,7 @@ import { useLLMServiceCatalog } from '../../../llm-services/hooks/useLLMServiceC
import { useLLMProviders } from '../../../llm-services/llm-providers';
import { useT } from '../../../locale';
import { buildProviderGroupedModelOptions, getServiceByOverride } from '../../../llm-services/utils';
+import { useAIConfigRepository } from '../../../repositories/hooks/useAIConfigRepository';
const { Meta } = Card;
@@ -58,8 +58,11 @@ const Shortcut: React.FC = ({
}) => {
const { size, mask } = style;
const [focus, setFocus] = useState(false);
-
- const { loading, aiEmployeesMap } = useAIEmployeesData();
+ const aiConfigRepository = useAIConfigRepository();
+ const { loading } = useRequest(async () => {
+ return aiConfigRepository.getAIEmployees();
+ });
+ const aiEmployeesMap = aiConfigRepository.getAIEmployeesMap();
const aiEmployee = aiEmployeesMap[username];
const { triggerTask } = useChatBoxActions();
@@ -104,7 +107,6 @@ const Shortcut: React.FC = ({
}}
onMouseLeave={() => setFocus(false)}
onClick={() => {
- setWebSearch(true);
triggerTask({ aiEmployee, tasks, auto });
if (context?.workContext?.length) {
addContextItems(context.workContext);
@@ -321,7 +323,8 @@ AIEmployeeShortcutModel.registerFlow({
};
},
uiSchema: async (ctx) => {
- const { aiEmployeesMap } = await ctx.aiEmployeesData;
+ await ctx.aiConfigRepository.getAIEmployees();
+ const aiEmployeesMap = ctx.aiConfigRepository.getAIEmployeesMap();
return {
profile: {
type: 'void',
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/hooks/useAIEmployeesData.ts b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/hooks/useAIEmployeesData.ts
deleted file mode 100644
index b3cb536044f..00000000000
--- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/hooks/useAIEmployeesData.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * This file is part of the NocoBase (R) project.
- * Copyright (c) 2020-2024 NocoBase Co., Ltd.
- * Authors: NocoBase Team.
- *
- * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
- * For more information, please refer to: https://www.nocobase.com/agreement.
- */
-
-import { useFlowEngine } from '@nocobase/flow-engine';
-import { AIEmployee } from '../types';
-import { useRequest } from '@nocobase/client';
-import { create } from 'zustand';
-import { createSelectors } from '../chatbox/stores/create-selectors';
-import { useEffect } from 'react';
-
-const store = create<{
- signal: boolean;
- sendSignal: () => void;
-}>()((set) => ({
- signal: false,
- sendSignal: () => set((state) => ({ signal: !state.signal })),
-}));
-const useSignal = createSelectors(store);
-
-export const useAIEmployeesData = () => {
- const flowEngine = useFlowEngine();
- const signal = useSignal.use.signal();
- const sendSignal = useSignal.use.sendSignal();
-
- const { loading, data, refreshAsync } = useRequest<{
- aiEmployees: AIEmployee[];
- aiEmployeesMap: {
- [username: string]: AIEmployee;
- };
- }>(() => flowEngine.context.aiEmployeesData);
- const aiEmployees = data?.aiEmployees || [];
- const aiEmployeesMap = data?.aiEmployeesMap || {};
-
- // AI员工管理页修改了数据后,ChatButton不会重新渲染
- // ChatButton所在父组件通过app.use注册,被useMemo包裹而且依赖是app,所以在父组件之外的hooks调用都不会触发ChatButton渲染
- // refresh方法被admin下的组件调用,admin并不和ChatButton在同一个组件树下。这时是没办法触发ChatButton渲染的,所以利用zustand来触发
- useEffect(() => {
- refreshAsync();
- }, [signal, refreshAsync]);
-
- return {
- loading,
- aiEmployees,
- aiEmployeesMap,
- refresh: () => {
- flowEngine.context.removeCache('aiEmployeesData');
- sendSignal();
- },
- };
-};
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/shortcuts/ShortcutList.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/shortcuts/ShortcutList.tsx
index c25b457a9c0..3cd1c6eb4e9 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/shortcuts/ShortcutList.tsx
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/shortcuts/ShortcutList.tsx
@@ -10,12 +10,12 @@
import { Button } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import { AddSubModelButton, FlowModelRenderer } from '@nocobase/flow-engine';
-import React from 'react';
+import React, { useEffect } from 'react';
import { useShortcuts } from './useShortcuts';
import { useDesignable } from '@nocobase/client';
import { AIEmployeeListItem } from '../AIEmployeeListItem';
import { observer } from '@nocobase/flow-engine';
-import { useAIEmployeesData } from '../hooks/useAIEmployeesData';
+import { useAIConfigRepository } from '../../repositories/hooks/useAIConfigRepository';
import { isHide } from '../built-in/utils';
export const ShortcutList: React.FC = observer(() => {
@@ -24,7 +24,13 @@ export const ShortcutList: React.FC = observer(() => {
const designMode = designable && !builtIn;
const hasShortcuts = model?.subModels?.shortcuts?.length > 0;
- const { loading, aiEmployees } = useAIEmployeesData();
+ const aiConfigRepository = useAIConfigRepository();
+ const loading = aiConfigRepository.aiEmployeesLoading;
+ const aiEmployees = aiConfigRepository.aiEmployees;
+
+ useEffect(() => {
+ aiConfigRepository.getAIEmployees();
+ }, [aiConfigRepository]);
return (
<>
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/index.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/index.tsx
index 52bcea6ee3b..c201f3aba13 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/index.tsx
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/index.tsx
@@ -31,8 +31,7 @@ import {
AIEmployeeButtonModel,
} from './ai-employees/flow/models';
import './ai-employees/flow/events';
-import { aiEmployeesData } from './ai-employees/flow/context';
-import { LLMServicesRepository } from './llm-services/LLMServicesRepository';
+import { AIConfigRepository } from './repositories/AIConfigRepository';
import { FlowModelsContext } from './ai-employees/context/flow-models';
import { DatasourceContext } from './ai-employees/context/datasource';
import { CodeEditorContext } from './ai-employees/context/code-editor';
@@ -138,10 +137,10 @@ export class PluginAIClient extends Plugin {
}
async setupAIFeatures() {
- this.app.flowEngine.context.defineProperty(...aiEmployeesData);
-
- const llmServicesRepository = new LLMServicesRepository(this.app.apiClient);
- this.app.flowEngine.context.defineProperty('llmServicesRepository', { value: llmServicesRepository });
+ const aiConfigRepository = new AIConfigRepository(this.app.apiClient, {
+ toolsManager: this.app.aiManager.toolsManager,
+ });
+ this.app.flowEngine.context.defineProperty('aiConfigRepository', { value: aiConfigRepository });
this.aiManager.registerLLMProvider('google-genai', googleGenAIProviderOptions);
this.aiManager.registerLLMProvider('openai', openaiResponsesProviderOptions);
@@ -193,12 +192,12 @@ export class PluginAIClient extends Plugin {
export default PluginAIClient;
export { ModelSelect, Chat };
export type { LLMProviderOptions, ToolOptions } from './manager/ai-manager';
-export type { ToolCall } from './ai-employees/types';
+export type { AIEmployee, ToolCall } from './ai-employees/types';
export * from './features';
export { AIEmployeeActionModel } from './ai-employees/flow/models/AIEmployeeActionModel';
-export { useAIEmployeesData } from './ai-employees/hooks/useAIEmployeesData';
export { useChatMessagesStore } from './ai-employees/chatbox/stores/chat-messages';
export { useChatBoxStore } from './ai-employees/chatbox/stores/chat-box';
export { useChatBoxActions } from './ai-employees/chatbox/hooks/useChatBoxActions';
+export { useAIConfigRepository } from './repositories/hooks/useAIConfigRepository';
export { ProfileCard } from './ai-employees/ProfileCard';
export { avatars } from './ai-employees/avatars';
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/llm-services/LLMServices.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/llm-services/LLMServices.tsx
index 1ab9901a213..d3d0e437609 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/llm-services/LLMServices.tsx
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/llm-services/LLMServices.tsx
@@ -43,7 +43,7 @@ import PluginAIClient from '..';
import { LLMTestFlight } from './component/LLMTestFlight';
import { EnabledModelsSelect } from './component/EnabledModelsSelect';
import { ModelOptionsSettings } from './component/ModelOptionsSettings';
-import { useLLMServicesRepository } from './hooks/useLLMServicesRepository';
+import { useAIConfigRepository } from '../repositories/hooks/useAIConfigRepository';
const useCreateFormProps = () => {
const form = useMemo(
@@ -91,7 +91,7 @@ const useCreateActionProps = () => {
const resource = useDataBlockResource();
const { refresh } = useDataBlockRequest();
const t = useT();
- const llmServicesRepo = useLLMServicesRepository();
+ const llmServicesRepo = useAIConfigRepository();
return {
type: 'primary',
@@ -102,7 +102,7 @@ const useCreateActionProps = () => {
values,
});
refresh();
- llmServicesRepo.refresh();
+ llmServicesRepo.refreshLLMServices();
message.success(t('Saved successfully'));
setVisible(false);
},
@@ -118,7 +118,7 @@ const useEditActionProps = () => {
const collection = useCollection();
const filterTk = collection.getFilterTargetKey();
const t = useT();
- const llmServicesRepo = useLLMServicesRepository();
+ const llmServicesRepo = useAIConfigRepository();
return {
type: 'primary',
@@ -130,7 +130,7 @@ const useEditActionProps = () => {
filterByTk: values[filterTk],
});
refresh();
- llmServicesRepo.refresh();
+ llmServicesRepo.refreshLLMServices();
message.success(t('Saved successfully'));
setVisible(false);
form.reset();
@@ -250,7 +250,7 @@ const EnabledSwitch: React.FC = observer(
const collection = useCollection();
const filterTk = collection.getFilterTargetKey();
const checked = field.value !== false;
- const llmServicesRepo = useLLMServicesRepository();
+ const llmServicesRepo = useAIConfigRepository();
return (
);
@@ -273,24 +273,24 @@ const EnabledSwitch: React.FC = observer(
const useLLMDestroyActionProps = () => {
const props = useDestroyActionProps();
- const llmServicesRepo = useLLMServicesRepository();
+ const llmServicesRepo = useAIConfigRepository();
return {
...props,
async onClick(e?, callBack?) {
await props.onClick(e, callBack);
- llmServicesRepo.refresh();
+ llmServicesRepo.refreshLLMServices();
},
};
};
const useLLMBulkDestroyActionProps = () => {
const props = useBulkDestroyActionProps();
- const llmServicesRepo = useLLMServicesRepository();
+ const llmServicesRepo = useAIConfigRepository();
return {
...props,
async onClick(e?, callBack?) {
await props.onClick(e, callBack);
- llmServicesRepo.refresh();
+ llmServicesRepo.refreshLLMServices();
},
};
};
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/llm-services/LLMServicesRepository.ts b/packages/plugins/@nocobase/plugin-ai/src/client/llm-services/LLMServicesRepository.ts
deleted file mode 100644
index 0f82ea41b46..00000000000
--- a/packages/plugins/@nocobase/plugin-ai/src/client/llm-services/LLMServicesRepository.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-/**
- * This file is part of the NocoBase (R) project.
- * Copyright (c) 2020-2024 NocoBase Co., Ltd.
- * Authors: NocoBase Team.
- *
- * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
- * For more information, please refer to: https://www.nocobase.com/agreement.
- */
-
-import { define, observable } from '@formily/reactive';
-
-export interface LLMServiceItem {
- llmService: string;
- llmServiceTitle: string;
- provider?: string;
- providerTitle?: string;
- enabledModels: { label: string; value: string }[];
- supportWebSearch?: boolean;
- isToolConflict?: boolean;
-}
-
-export class LLMServicesRepository {
- services: LLMServiceItem[] = [];
- loading = false;
- private apiClient: any;
- private loadPromise: Promise | null = null;
-
- constructor(apiClient: any) {
- this.apiClient = apiClient;
- define(this, {
- services: observable.shallow,
- loading: observable.ref,
- });
- }
-
- async load() {
- if (this.loadPromise) return this.loadPromise;
- if (this.services.length > 0) return;
- this.loadPromise = this.fetchServices();
- return this.loadPromise;
- }
-
- async refresh() {
- this.loadPromise = this.fetchServices();
- return this.loadPromise;
- }
-
- private async fetchServices() {
- this.loading = true;
- try {
- const res = await this.apiClient.resource('ai').listAllEnabledModels();
- const data = res?.data?.data;
- this.services = Array.isArray(data) ? data : [];
- } catch {
- this.services = [];
- } finally {
- this.loading = false;
- this.loadPromise = null;
- }
- }
-}
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/llm-services/hooks/useLLMServiceCatalog.ts b/packages/plugins/@nocobase/plugin-ai/src/client/llm-services/hooks/useLLMServiceCatalog.ts
index 0a0d7f7d0e6..489876286e3 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/llm-services/hooks/useLLMServiceCatalog.ts
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/llm-services/hooks/useLLMServiceCatalog.ts
@@ -8,18 +8,18 @@
*/
import { useEffect, useMemo } from 'react';
-import { useLLMServicesRepository } from './useLLMServicesRepository';
+import { useAIConfigRepository } from '../../repositories/hooks/useAIConfigRepository';
import { getAllModelsWithLabel } from '../utils';
export const useLLMServiceCatalog = () => {
- const repo = useLLMServicesRepository();
+ const repo = useAIConfigRepository();
useEffect(() => {
- repo.load();
+ repo.getLLMServices();
}, [repo]);
- const services = repo.services;
- const loading = repo.loading;
+ const services = repo.llmServices;
+ const loading = repo.llmServicesLoading;
const allModelsWithLabel = useMemo(() => getAllModelsWithLabel(services), [services]);
const allModels = useMemo(
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/llm-services/utils.ts b/packages/plugins/@nocobase/plugin-ai/src/client/llm-services/utils.ts
index ddc45caf0b9..702784c9c73 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/llm-services/utils.ts
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/llm-services/utils.ts
@@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-import { LLMServiceItem } from './LLMServicesRepository';
+import { LLMServiceItem } from '../repositories/AIConfigRepository';
export type ModelWithLabel = {
llmService: string;
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/repositories/AIConfigRepository.ts b/packages/plugins/@nocobase/plugin-ai/src/client/repositories/AIConfigRepository.ts
new file mode 100644
index 00000000000..70622d1f61c
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/repositories/AIConfigRepository.ts
@@ -0,0 +1,213 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+import { define, observable } from '@formily/reactive';
+import { ToolsEntry, type ToolsManager } from '@nocobase/client';
+import { AIEmployee } from '../ai-employees/types';
+
+export interface LLMServiceItem {
+ llmService: string;
+ llmServiceTitle: string;
+ provider?: string;
+ providerTitle?: string;
+ enabledModels: { label: string; value: string }[];
+ supportWebSearch?: boolean;
+ isToolConflict?: boolean;
+}
+
+export class AIConfigRepository {
+ llmServices = observable.shallow([]);
+ llmServicesLoading = false;
+ aiEmployees = observable.shallow([]);
+ aiEmployeesLoading = false;
+ aiTools = observable.shallow([]);
+ aiToolsLoading = false;
+
+ private llmServicesLoaded = false;
+ private aiEmployeesLoaded = false;
+ private aiToolsLoaded = false;
+ private llmServicesInFlight: Promise | null = null;
+ private aiEmployeesInFlight: Promise | null = null;
+ private aiToolsInFlight: Promise | null = null;
+
+ constructor(
+ private readonly apiClient: any,
+ private readonly options?: { toolsManager?: Pick },
+ ) {
+ define(this, {
+ llmServices: observable.shallow,
+ llmServicesLoading: observable.ref,
+ aiEmployees: observable.shallow,
+ aiEmployeesLoading: observable.ref,
+ aiTools: observable.shallow,
+ aiToolsLoading: observable.ref,
+ });
+ }
+
+ async getLLMServices(): Promise {
+ if (this.llmServicesInFlight) {
+ return this.llmServicesInFlight;
+ }
+ if (this.llmServicesLoaded) {
+ return this.llmServices;
+ }
+ return this.startRefresh(
+ this.llmServicesInFlight,
+ (promise) => {
+ this.llmServicesInFlight = promise;
+ },
+ () => this.doRefreshLLMServices(),
+ () => this.llmServices,
+ );
+ }
+
+ async refreshLLMServices(): Promise {
+ return this.startRefresh(
+ this.llmServicesInFlight,
+ (promise) => {
+ this.llmServicesInFlight = promise;
+ },
+ () => this.doRefreshLLMServices(),
+ () => this.llmServices,
+ );
+ }
+
+ async getAIEmployees(): Promise {
+ if (this.aiEmployeesInFlight) {
+ return this.aiEmployeesInFlight;
+ }
+ if (this.aiEmployeesLoaded) {
+ return this.aiEmployees;
+ }
+ return this.startRefresh(
+ this.aiEmployeesInFlight,
+ (promise) => {
+ this.aiEmployeesInFlight = promise;
+ },
+ () => this.doRefreshAIEmployees(),
+ () => this.aiEmployees,
+ );
+ }
+
+ async refreshAIEmployees(): Promise {
+ return this.startRefresh(
+ this.aiEmployeesInFlight,
+ (promise) => {
+ this.aiEmployeesInFlight = promise;
+ },
+ () => this.doRefreshAIEmployees(),
+ () => this.aiEmployees,
+ );
+ }
+
+ getAIEmployeesMap(): Record {
+ return this.aiEmployees.reduce>((acc, aiEmployee) => {
+ acc[aiEmployee.username] = aiEmployee;
+ return acc;
+ }, {});
+ }
+
+ async getAITools(): Promise {
+ if (this.aiToolsInFlight) {
+ return this.aiToolsInFlight;
+ }
+ if (this.aiToolsLoaded) {
+ return this.aiTools;
+ }
+ return this.startRefresh(
+ this.aiToolsInFlight,
+ (promise) => {
+ this.aiToolsInFlight = promise;
+ },
+ () => this.doRefreshAITools(),
+ () => this.aiTools,
+ );
+ }
+
+ async refreshAITools(): Promise {
+ return this.startRefresh(
+ this.aiToolsInFlight,
+ (promise) => {
+ this.aiToolsInFlight = promise;
+ },
+ () => this.doRefreshAITools(),
+ () => this.aiTools,
+ );
+ }
+
+ private startRefresh(
+ inFlight: Promise | null,
+ setInFlight: (promise: Promise | null) => void,
+ refresh: () => Promise,
+ getData: () => T,
+ ): Promise {
+ if (inFlight) {
+ return inFlight;
+ }
+ const promise = refresh()
+ .then(() => getData())
+ .finally(() => {
+ setInFlight(null);
+ });
+ setInFlight(promise);
+ return promise;
+ }
+
+ private async doRefreshLLMServices() {
+ this.llmServicesLoading = true;
+ try {
+ const res = await this.apiClient.resource('ai').listAllEnabledModels();
+ const data = Array.isArray(res?.data?.data) ? (res.data.data as LLMServiceItem[]) : [];
+ this.llmServices = data;
+ this.llmServicesLoaded = true;
+ } catch {
+ this.llmServices = [];
+ this.llmServicesLoaded = false;
+ } finally {
+ this.llmServicesLoading = false;
+ }
+ }
+
+ private async doRefreshAIEmployees() {
+ this.aiEmployeesLoading = true;
+ try {
+ const aiEmployees: AIEmployee[] = await this.apiClient
+ .resource('aiEmployees')
+ .listByUser()
+ .then((res) => res?.data?.data);
+ this.aiEmployees = aiEmployees || [];
+ this.aiEmployeesLoaded = true;
+ } catch {
+ this.aiEmployees = [];
+ this.aiEmployeesLoaded = false;
+ } finally {
+ this.aiEmployeesLoading = false;
+ }
+ }
+
+ private async doRefreshAITools() {
+ this.aiToolsLoading = true;
+ try {
+ let tools: ToolsEntry[] = [];
+ if (this.options?.toolsManager) {
+ tools = await this.options.toolsManager.listTools();
+ } else {
+ const { data: res } = await this.apiClient.resource('aiTools').list({});
+ tools = Array.isArray(res?.data) ? res.data : [];
+ }
+ this.aiTools = tools;
+ this.aiToolsLoaded = true;
+ } catch {
+ this.aiTools = [];
+ this.aiToolsLoaded = false;
+ } finally {
+ this.aiToolsLoading = false;
+ }
+ }
+}
diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/llm-services/hooks/useLLMServicesRepository.ts b/packages/plugins/@nocobase/plugin-ai/src/client/repositories/hooks/useAIConfigRepository.ts
similarity index 64%
rename from packages/plugins/@nocobase/plugin-ai/src/client/llm-services/hooks/useLLMServicesRepository.ts
rename to packages/plugins/@nocobase/plugin-ai/src/client/repositories/hooks/useAIConfigRepository.ts
index 48cef69a298..2e727bdc010 100644
--- a/packages/plugins/@nocobase/plugin-ai/src/client/llm-services/hooks/useLLMServicesRepository.ts
+++ b/packages/plugins/@nocobase/plugin-ai/src/client/repositories/hooks/useAIConfigRepository.ts
@@ -8,8 +8,8 @@
*/
import { useFlowEngine } from '@nocobase/flow-engine';
-import { LLMServicesRepository } from '../LLMServicesRepository';
+import { AIConfigRepository } from '../AIConfigRepository';
-export const useLLMServicesRepository = (): LLMServicesRepository => {
- return useFlowEngine().context.llmServicesRepository;
+export const useAIConfigRepository = (): AIConfigRepository => {
+ return useFlowEngine().context.aiConfigRepository;
};
diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/client/flow/components/DaraButton.tsx b/packages/plugins/@nocobase/plugin-data-visualization/src/client/flow/components/DaraButton.tsx
index bd0d578e7fa..9d6d3d92b39 100644
--- a/packages/plugins/@nocobase/plugin-data-visualization/src/client/flow/components/DaraButton.tsx
+++ b/packages/plugins/@nocobase/plugin-data-visualization/src/client/flow/components/DaraButton.tsx
@@ -12,19 +12,20 @@ import { useT } from '../../locale';
import { Avatar, Popover } from 'antd';
import {
useChatMessagesStore,
- useAIEmployeesData,
+ useAIConfigRepository,
useChatBoxStore,
useChatBoxActions,
ProfileCard,
avatars,
} from '@nocobase/plugin-ai/client';
import type { EditorRef } from '@nocobase/client';
-import { DEFAULT_DATA_SOURCE_KEY } from '@nocobase/client';
+import { observer } from '@nocobase/flow-engine';
import type { FlowSettingsContext } from '@nocobase/flow-engine';
-export const DaraButton: React.FC<{ ctx: FlowSettingsContext }> = ({ ctx }) => {
+export const DaraButton: React.FC<{ ctx: FlowSettingsContext }> = observer(({ ctx }) => {
const t = useT();
- const { aiEmployees } = useAIEmployeesData();
+ const aiConfigRepository = useAIConfigRepository();
+ const aiEmployees = aiConfigRepository.aiEmployees;
const aiEmployee = aiEmployees?.find((e) => e.username === 'dara');
const setEditorRef = useChatMessagesStore.use.setEditorRef();
const setCurrentEditorRefUid = useChatMessagesStore.use.setCurrentEditorRefUid();
@@ -85,11 +86,15 @@ export const DaraButton: React.FC<{ ctx: FlowSettingsContext }> = ({ ctx })
logs: [],
};
+ React.useEffect(() => {
+ aiConfigRepository.getAIEmployees();
+ }, [aiConfigRepository]);
+
React.useEffect(() => {
setEditorRef(uid, panelRef);
setCurrentEditorRefUid(uid);
return () => setEditorRef(uid, null);
- }, [uid]);
+ }, [uid, setEditorRef, setCurrentEditorRefUid]);
const systemPrompt =
'If you are not in SQL/Custom mode, first call the tool viz.switchModes; after editing SQL, if you need field samples, call the tool viz.runQuery. Use query.sqlDatasource as the current data source key when executing SQL. Do not render chart previews directly in the chat window.';
@@ -159,6 +164,6 @@ export const DaraButton: React.FC<{ ctx: FlowSettingsContext }> = ({ ctx })
/>
);
-};
+});
export default DaraButton;
From 16b09b38ded88549d67d37f2f11f211e179eaa9f Mon Sep 17 00:00:00 2001
From: chenos
Date: Thu, 26 Feb 2026 20:55:09 +0800
Subject: [PATCH 2/3] feat: support summary configuration for table (#8721)
---
.../core/client/src/flow/models/blocks/table/TableBlockModel.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/core/client/src/flow/models/blocks/table/TableBlockModel.tsx b/packages/core/client/src/flow/models/blocks/table/TableBlockModel.tsx
index 4ffbf28005a..2a1a23802e1 100644
--- a/packages/core/client/src/flow/models/blocks/table/TableBlockModel.tsx
+++ b/packages/core/client/src/flow/models/blocks/table/TableBlockModel.tsx
@@ -984,6 +984,7 @@ const HighPerformanceTable = React.memo(
return (
Date: Thu, 26 Feb 2026 23:21:39 +0800
Subject: [PATCH 3/3] fix(plugin-javascript): fix test cases fail on windows
(#8722)
---
.../plugin-workflow-javascript/src/server/Vm.js | 11 ++++++++++-
.../src/server/__tests__/instruction.test.ts | 6 +++---
2 files changed, 13 insertions(+), 4 deletions(-)
diff --git a/packages/plugins/@nocobase/plugin-workflow-javascript/src/server/Vm.js b/packages/plugins/@nocobase/plugin-workflow-javascript/src/server/Vm.js
index e96f0cbbb1b..ff37a5711c3 100644
--- a/packages/plugins/@nocobase/plugin-workflow-javascript/src/server/Vm.js
+++ b/packages/plugins/@nocobase/plugin-workflow-javascript/src/server/Vm.js
@@ -1,3 +1,12 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
const { parentPort, workerData } = require('node:worker_threads');
const { Script } = require('node:vm');
const Path = require('node:path');
@@ -7,7 +16,7 @@ let timer = null;
function customRequire(m) {
const configuredModules = (process.env.WORKFLOW_SCRIPT_MODULES?.split(',') ?? []).filter(Boolean);
let mainName;
- if (m.startsWith('/')) {
+ if (Path.isAbsolute(m)) {
// absolute path
mainName = m;
} else if (m.startsWith('.')) {
diff --git a/packages/plugins/@nocobase/plugin-workflow-javascript/src/server/__tests__/instruction.test.ts b/packages/plugins/@nocobase/plugin-workflow-javascript/src/server/__tests__/instruction.test.ts
index 12382c2dda1..6fed7d9acc9 100644
--- a/packages/plugins/@nocobase/plugin-workflow-javascript/src/server/__tests__/instruction.test.ts
+++ b/packages/plugins/@nocobase/plugin-workflow-javascript/src/server/__tests__/instruction.test.ts
@@ -30,7 +30,7 @@ describe('workflow > instructions > script', () => {
beforeEach(async () => {
originalEnv = process.env.WORKFLOW_SCRIPT_MODULES;
- const mathjsPath = Path.resolve(process.env.PWD, 'node_modules', 'mathjs');
+ const mathjsPath = Path.resolve(process.cwd(), 'node_modules', 'mathjs');
const testModulePath = '.' + Path.sep + 'node_modules' + Path.sep + 'mathjs';
process.env.WORKFLOW_SCRIPT_MODULES = `path,crypto,lodash,dayjs,http,axios,node:timers,node:process,fs,@nocobase/utils,${mathjsPath},${testModulePath}`;
@@ -321,7 +321,7 @@ describe('workflow > instructions > script', () => {
const script = `
const process = require('node:process');
const Path = require('path');
- const math = require(Path.resolve(process.env.PWD, 'node_modules', 'mathjs'));
+ const math = require(Path.resolve(process.cwd(), 'node_modules', 'mathjs'));
return math.evaluate('1+1');
`;
const result = await ScriptInstruction.run(script, [], { logger });
@@ -334,7 +334,7 @@ describe('workflow > instructions > script', () => {
const script = `
const process = require('node:process');
const Path = require('path');
- const { ABS } = require(Path.resolve(process.env.PWD, 'node_modules', '@formulajs', 'formulajs'));
+ const { ABS } = require(Path.resolve(process.cwd(), 'node_modules', '@formulajs', 'formulajs'));
return ABS(-1);
`;
const result = await ScriptInstruction.run(script, [], { logger });