Merge branch 'main' into feature/OpenAI-Response-API

This commit is contained in:
Henry 2025-08-07 17:30:55 +01:00
commit 7867830ed4
10 changed files with 176 additions and 61 deletions

View File

@ -18,7 +18,7 @@ If you like to persist your data (flows, logs, credentials, storage), set these
- SECRETKEY_PATH=/root/.flowise
- BLOB_STORAGE_PATH=/root/.flowise/storage
Flowise also support different environment variables to configure your instance. Read [more](https://docs.flowiseai.com/environment-variables)
Flowise also support different environment variables to configure your instance. Read [more](https://docs.flowiseai.com/configuration/environment-variables)
## Queue Mode:

View File

@ -13,7 +13,7 @@
[English](../README.md) | 繁體中文 | [简体中文](./README-ZH.md) | [日本語](./README-JA.md) | [한국어](./README-KR.md)
<h3>可視化建 AI/LLM 流程</h3>
<h3>可視化建 AI/LLM 流程</h3>
<a href="https://github.com/FlowiseAI/Flowise">
<img width="100%" src="https://github.com/FlowiseAI/Flowise/blob/main/images/flowise_agentflow.gif?raw=true"></a>
@ -37,16 +37,16 @@
### Docker Compose
1. 克隆 Flowise 項目
2. 進入項目根目錄的 `docker` 文件
3. 複製 `.env.example` 文件,貼到相同位置,並重命名為 `.env` 文件
1. 複製 Flowise 專案
2. 進入專案根目錄的 `docker` 資料
3. 複製 `.env.example` 文件,貼到相同位置,並重命名為 `.env` 文件
4. `docker compose up -d`
5. 打開 [http://localhost:3000](http://localhost:3000)
6. 您可以`docker compose stop` 停止容器
6. 您可以`docker compose stop` 停止容器
### Docker 映像
1. 本地建映像:
1. 本地建映像:
```bash
docker build --no-cache -t flowise .
```
@ -63,7 +63,7 @@
## 👨‍💻 開發者
Flowise 在單個 mono 存儲庫中有 3 個不同的模塊
Flowise 在單個 mono 儲存庫中有 3 個不同的模組
- `server`: 提供 API 邏輯的 Node 後端
- `ui`: React 前端
@ -79,33 +79,33 @@ Flowise 在單個 mono 存儲庫中有 3 個不同的模塊。
### 設置
1. 克隆存儲
1. 複製儲存
```bash
git clone https://github.com/FlowiseAI/Flowise.git
```
2. 進入儲庫文件夾
2. 進入儲庫文件夾
```bash
cd Flowise
```
3. 安裝所有模的所有依賴項:
3. 安裝所有模的所有依賴項:
```bash
pnpm install
```
4. 構建所有代碼:
4. 建置所有程式碼:
```bash
pnpm build
```
<details>
<summary>退出代碼 134JavaScript 堆內存不足</summary>
如果在運行上述 `build` 腳本時遇到此錯誤,請嘗試增加 Node.js 大小並重新運行腳本:
<summary>Exit code 134JavaScript heap out of memory</summary>
如果在運行上述 `build` 腳本時遇到此錯誤,請嘗試增加 Node.js 中的 Heap 記憶體大小並重新運行腳本:
export NODE_OPTIONS="--max-old-space-size=4096"
pnpm build
@ -118,9 +118,9 @@ Flowise 在單個 mono 存儲庫中有 3 個不同的模塊。
pnpm start
```
您現在可以訪問 [http://localhost:3000](http://localhost:3000)
您現在可以開啟 [http://localhost:3000](http://localhost:3000)
6. 對於開發建:
6. 對於開發建
- 在 `packages/ui` 中創建 `.env` 文件並指定 `VITE_PORT`(參考 `.env.example`
- 在 `packages/server` 中創建 `.env` 文件並指定 `PORT`(參考 `.env.example`
@ -130,19 +130,19 @@ Flowise 在單個 mono 存儲庫中有 3 個不同的模塊。
pnpm dev
```
任何代碼更改都會自動重新加載應用程序 [http://localhost:8080](http://localhost:8080)
任何程式碼更改都會自動重新加載應用程式 [http://localhost:8080](http://localhost:8080)
## 🌱 環境變
## 🌱 環境變
Flowise 支持不同的環境變來配置您的實例。您可以在 `packages/server` 文件夾中的 `.env` 文件中指定以下變。閱讀 [更多](https://github.com/FlowiseAI/Flowise/blob/main/CONTRIBUTING.md#-env-variables)
Flowise 支持不同的環境變來配置您的實例。您可以在 `packages/server` 文件夾中的 `.env` 文件中指定以下變。閱讀 [更多](https://github.com/FlowiseAI/Flowise/blob/main/CONTRIBUTING.md#-env-variables)
## 📖 文檔
[Flowise 文檔](https://docs.flowiseai.com/)
## 🌐 自我托管
## 🌐 自行架設
在您現有的基礎設施中部署 Flowise 自我托管,我們支持各種 [部署](https://docs.flowiseai.com/configuration/deployment)
在您現有的基礎設施中部署 Flowise我們支持各種自行架設選項 [部署](https://docs.flowiseai.com/configuration/deployment)
- [AWS](https://docs.flowiseai.com/configuration/deployment/aws)
- [Azure](https://docs.flowiseai.com/configuration/deployment/azure)
@ -178,9 +178,9 @@ Flowise 支持不同的環境變量來配置您的實例。您可以在 `package
</details>
## ☁️ Flowise 雲
## ☁️ Flowise 雲端平台
[開始使用 Flowise 雲](https://flowiseai.com/)
[開始使用 Flowise 雲端平台](https://flowiseai.com/)
## 🙋 支持
@ -194,9 +194,9 @@ Flowise 支持不同的環境變量來配置您的實例。您可以在 `package
<img src="https://contrib.rocks/image?repo=FlowiseAI/Flowise" />
</a>
請參閱 [貢獻指南](../CONTRIBUTING.md)。如果您有任何問題或問題,請過 [Discord](https://discord.gg/jbaHfsRVBW) 與我們聯繫。
請參閱 [貢獻指南](../CONTRIBUTING.md)。如果您有任何問題或問題,請過 [Discord](https://discord.gg/jbaHfsRVBW) 與我們聯繫。
[![Star History Chart](https://api.star-history.com/svg?repos=FlowiseAI/Flowise&type=Timeline)](https://star-history.com/#FlowiseAI/Flowise&Date)
## 📄 許可證
存儲庫中的源代碼根據 [Apache 許可證版本 2.0](../LICENSE.md) 提供
儲存庫中的原始碼根據 [Apache 2.0 授權條款](../LICENSE.md) 授權使用

View File

@ -746,6 +746,14 @@
{
"name": "groqChat",
"models": [
{
"label": "openai/gpt-oss-20b",
"name": "openai/gpt-oss-20b"
},
{
"label": "openai/gpt-oss-120b",
"name": "openai/gpt-oss-120b"
},
{
"label": "meta-llama/llama-4-maverick-17b-128e-instruct",
"name": "meta-llama/llama-4-maverick-17b-128e-instruct"

View File

@ -4,6 +4,25 @@ import { GoogleGenerativeAIEmbeddings, GoogleGenerativeAIEmbeddingsParams } from
import { TaskType } from '@google/generative-ai'
import { MODEL_TYPE, getModels } from '../../../src/modelLoader'
class GoogleGenerativeAIEmbeddingsWithStripNewLines extends GoogleGenerativeAIEmbeddings {
stripNewLines: boolean
constructor(params: GoogleGenerativeAIEmbeddingsParams & { stripNewLines?: boolean }) {
super(params)
this.stripNewLines = params.stripNewLines ?? false
}
async embedDocuments(texts: string[]): Promise<number[][]> {
const processedTexts = this.stripNewLines ? texts.map((text) => text.replace(/\n/g, ' ')) : texts
return super.embedDocuments(processedTexts)
}
async embedQuery(text: string): Promise<number[]> {
const processedText = this.stripNewLines ? text.replace(/\n/g, ' ') : text
return super.embedQuery(processedText)
}
}
class GoogleGenerativeAIEmbedding_Embeddings implements INode {
label: string
name: string
@ -24,7 +43,7 @@ class GoogleGenerativeAIEmbedding_Embeddings implements INode {
this.icon = 'GoogleGemini.svg'
this.category = 'Embeddings'
this.description = 'Google Generative API to generate embeddings for a given text'
this.baseClasses = [this.type, ...getBaseClasses(GoogleGenerativeAIEmbeddings)]
this.baseClasses = [this.type, ...getBaseClasses(GoogleGenerativeAIEmbeddingsWithStripNewLines)]
this.credential = {
label: 'Connect Credential',
name: 'credential',
@ -55,6 +74,14 @@ class GoogleGenerativeAIEmbedding_Embeddings implements INode {
{ label: 'CLUSTERING', name: 'CLUSTERING' }
],
default: 'TASK_TYPE_UNSPECIFIED'
},
{
label: 'Strip New Lines',
name: 'stripNewLines',
type: 'boolean',
optional: true,
additionalParams: true,
description: 'Remove new lines from input text before embedding to reduce token count'
}
]
}
@ -71,6 +98,7 @@ class GoogleGenerativeAIEmbedding_Embeddings implements INode {
const modelName = nodeData.inputs?.modelName as string
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const apiKey = getCredentialParam('googleGenerativeAPIKey', credentialData, nodeData)
const stripNewLines = nodeData.inputs?.stripNewLines as boolean
let taskType: TaskType
switch (nodeData.inputs?.tasktype as string) {
@ -93,13 +121,14 @@ class GoogleGenerativeAIEmbedding_Embeddings implements INode {
taskType = TaskType.TASK_TYPE_UNSPECIFIED
break
}
const obj: GoogleGenerativeAIEmbeddingsParams = {
const obj: GoogleGenerativeAIEmbeddingsParams & { stripNewLines?: boolean } = {
apiKey: apiKey,
modelName: modelName,
taskType: taskType
taskType: taskType,
stripNewLines
}
const model = new GoogleGenerativeAIEmbeddings(obj)
const model = new GoogleGenerativeAIEmbeddingsWithStripNewLines(obj)
return model
}
}

View File

@ -4,6 +4,25 @@ import { ICommonObject, INode, INodeData, INodeOptionsValue, INodeParams } from
import { MODEL_TYPE, getModels, getRegions } from '../../../src/modelLoader'
import { getBaseClasses } from '../../../src/utils'
class VertexAIEmbeddingsWithStripNewLines extends VertexAIEmbeddings {
stripNewLines: boolean
constructor(params: GoogleVertexAIEmbeddingsInput & { stripNewLines?: boolean }) {
super(params)
this.stripNewLines = params.stripNewLines ?? false
}
async embedDocuments(texts: string[]): Promise<number[][]> {
const processedTexts = this.stripNewLines ? texts.map((text) => text.replace(/\n/g, ' ')) : texts
return super.embedDocuments(processedTexts)
}
async embedQuery(text: string): Promise<number[]> {
const processedText = this.stripNewLines ? text.replace(/\n/g, ' ') : text
return super.embedQuery(processedText)
}
}
class GoogleVertexAIEmbedding_Embeddings implements INode {
label: string
name: string
@ -24,7 +43,7 @@ class GoogleVertexAIEmbedding_Embeddings implements INode {
this.icon = 'GoogleVertex.svg'
this.category = 'Embeddings'
this.description = 'Google vertexAI API to generate embeddings for a given text'
this.baseClasses = [this.type, ...getBaseClasses(VertexAIEmbeddings)]
this.baseClasses = [this.type, ...getBaseClasses(VertexAIEmbeddingsWithStripNewLines)]
this.credential = {
label: 'Connect Credential',
name: 'credential',
@ -49,6 +68,14 @@ class GoogleVertexAIEmbedding_Embeddings implements INode {
type: 'asyncOptions',
loadMethod: 'listRegions',
optional: true
},
{
label: 'Strip New Lines',
name: 'stripNewLines',
type: 'boolean',
optional: true,
additionalParams: true,
description: 'Remove new lines from input text before embedding to reduce token count'
}
]
}
@ -66,9 +93,11 @@ class GoogleVertexAIEmbedding_Embeddings implements INode {
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const modelName = nodeData.inputs?.modelName as string
const region = nodeData.inputs?.region as string
const stripNewLines = nodeData.inputs?.stripNewLines as boolean
const obj: GoogleVertexAIEmbeddingsInput = {
model: modelName
const obj: GoogleVertexAIEmbeddingsInput & { stripNewLines?: boolean } = {
model: modelName,
stripNewLines
}
const authOptions = await buildGoogleCredentials(nodeData, options)
@ -76,7 +105,7 @@ class GoogleVertexAIEmbedding_Embeddings implements INode {
if (region) obj.location = region
const model = new VertexAIEmbeddings(obj)
const model = new VertexAIEmbeddingsWithStripNewLines(obj)
return model
}
}

View File

@ -9,6 +9,9 @@ import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
import { checkUsageLimit } from '../../utils/quotaUsage'
import { RateLimiterManager } from '../../utils/rateLimit'
import { getPageAndLimitParams } from '../../utils/pagination'
import { WorkspaceUserErrorMessage, WorkspaceUserService } from '../../enterprise/services/workspace-user.service'
import { QueryRunner } from 'typeorm'
import { GeneralErrorMessage } from '../../utils/constants'
const checkIfChatflowIsValidForStreaming = async (req: Request, res: Response, next: NextFunction) => {
try {
@ -197,6 +200,7 @@ const updateChatflow = async (req: Request, res: Response, next: NextFunction) =
}
const getSinglePublicChatflow = async (req: Request, res: Response, next: NextFunction) => {
let queryRunner: QueryRunner | undefined
try {
if (typeof req.params === 'undefined' || !req.params.id) {
throw new InternalFlowiseError(
@ -204,10 +208,23 @@ const getSinglePublicChatflow = async (req: Request, res: Response, next: NextFu
`Error: chatflowsController.getSinglePublicChatflow - id not provided!`
)
}
const apiResponse = await chatflowsService.getSinglePublicChatflow(req.params.id)
return res.json(apiResponse)
const chatflow = await chatflowsService.getChatflowById(req.params.id)
if (!chatflow) return res.status(StatusCodes.NOT_FOUND).json({ message: 'Chatflow not found' })
if (chatflow.isPublic) return res.status(StatusCodes.OK).json(chatflow)
if (!req.user) return res.status(StatusCodes.UNAUTHORIZED).json({ message: GeneralErrorMessage.UNAUTHORIZED })
queryRunner = getRunningExpressApp().AppDataSource.createQueryRunner()
const workspaceUserService = new WorkspaceUserService()
const workspaceUser = await workspaceUserService.readWorkspaceUserByUserId(req.user.id, queryRunner)
if (workspaceUser.length === 0)
return res.status(StatusCodes.NOT_FOUND).json({ message: WorkspaceUserErrorMessage.WORKSPACE_USER_NOT_FOUND })
const workspaceIds = workspaceUser.map((user) => user.workspaceId)
if (!workspaceIds.includes(chatflow.workspaceId))
return res.status(StatusCodes.BAD_REQUEST).json({ message: 'You are not in the workspace that owns this chatflow' })
return res.status(StatusCodes.OK).json(chatflow)
} catch (error) {
next(error)
} finally {
if (queryRunner) await queryRunner.release()
}
}

View File

@ -339,31 +339,6 @@ const updateChatflow = async (
}
}
// Get specific chatflow via id (PUBLIC endpoint, used when sharing chatbot link)
const getSinglePublicChatflow = async (chatflowId: string): Promise<any> => {
try {
const appServer = getRunningExpressApp()
const dbResponse = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({
id: chatflowId
})
if (dbResponse && dbResponse.isPublic) {
return dbResponse
} else if (dbResponse && !dbResponse.isPublic) {
throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, `Unauthorized`)
}
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `Chatflow ${chatflowId} not found`)
} catch (error) {
if (error instanceof InternalFlowiseError && error.statusCode === StatusCodes.UNAUTHORIZED) {
throw error
} else {
throw new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR,
`Error: chatflowsService.getSinglePublicChatflow - ${getErrorMessage(error)}`
)
}
}
}
// Get specific chatflow chatbotConfig via id (PUBLIC endpoint, used to retrieve config for embedded chat)
// Safe as public endpoint as chatbotConfig doesn't contain sensitive credential
const getSinglePublicChatbotConfig = async (chatflowId: string): Promise<any> => {
@ -438,7 +413,6 @@ export default {
getChatflowById,
saveChatflow,
updateChatflow,
getSinglePublicChatflow,
getSinglePublicChatbotConfig,
checkIfChatflowHasChanged,
getAllChatflowsCountByOrganization

View File

@ -26,6 +26,7 @@ import marketplacesService from '../marketplaces'
import toolsService from '../tools'
import variableService from '../variables'
import { Platform } from '../../Interface'
import { sanitizeNullBytes } from '../../utils/sanitize.util'
type ExportInput = {
agentflow: boolean
@ -753,6 +754,8 @@ const importData = async (importData: ExportData, orgId: string, activeWorkspace
importData = await replaceDuplicateIdsForVariable(queryRunner, importData, importData.Variable)
}
importData = sanitizeNullBytes(importData)
await queryRunner.startTransaction()
if (importData.AgentFlow.length > 0) await queryRunner.manager.save(ChatFlow, importData.AgentFlow)

View File

@ -1403,6 +1403,29 @@ export const executeAgentFlow = async ({
}
}
// Check if startState has been overridden from overrideConfig.startState and is enabled
const startAgentflowNode = nodes.find((node) => node.data.name === 'startAgentflow')
const isStartStateEnabled =
nodeOverrides && startAgentflowNode
? nodeOverrides[startAgentflowNode.data.label]?.find((param: any) => param.name === 'startState')?.enabled ?? false
: false
if (isStartStateEnabled && overrideConfig?.startState) {
if (Array.isArray(overrideConfig.startState)) {
// Handle array format: [{"key": "foo", "value": "foo4"}]
const overrideStateObj: ICommonObject = {}
for (const item of overrideConfig.startState) {
if (item.key && item.value !== undefined) {
overrideStateObj[item.key] = item.value
}
}
previousState = { ...previousState, ...overrideStateObj }
} else if (typeof overrideConfig.startState === 'object') {
// Object override: "startState": {...}
previousState = { ...previousState, ...overrideConfig.startState }
}
}
agentflowRuntime.state = previousState
}

View File

@ -0,0 +1,32 @@
export function sanitizeNullBytes(obj: any): any {
const stack = [obj]
while (stack.length) {
const current = stack.pop()
if (Array.isArray(current)) {
for (let i = 0; i < current.length; i++) {
const val = current[i]
if (typeof val === 'string') {
// eslint-disable-next-line no-control-regex
current[i] = val.replace(/\u0000/g, '')
} else if (val && typeof val === 'object') {
stack.push(val)
}
}
} else if (current && typeof current === 'object') {
for (const key in current) {
if (!Object.hasOwnProperty.call(current, key)) continue
const val = current[key]
if (typeof val === 'string') {
// eslint-disable-next-line no-control-regex
current[key] = val.replace(/\u0000/g, '')
} else if (val && typeof val === 'object') {
stack.push(val)
}
}
}
}
return obj
}