4.8-fix (#1305)
Some checks failed
Build FastGPT images in Personal warehouse / build-fastgpt-images (push) Waiting to run
Deploy image by kubeconfig / build-fastgpt-docs-images (push) Has been cancelled
Deploy image to vercel / deploy-production (push) Has been cancelled
Deploy image by kubeconfig / update-docs-image (push) Has been cancelled

* fix if-else find variables (#92)

* fix if-else find variables

* change workflow output type

* fix tooltip style

* fix

* 4.8 (#93)

* api middleware

* perf: app version histories

* faq

* perf: value type show

* fix: ts

* fix: Run the same node multiple times

* feat: auto save workflow

* perf: auto save workflow

---------

Co-authored-by: heheer <71265218+newfish-cmyk@users.noreply.github.com>
This commit is contained in:
Archer 2024-04-27 12:21:01 +08:00 committed by GitHub
parent c8412e7dc9
commit d407e87dd9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
87 changed files with 1607 additions and 1779 deletions

29
.vscode/nextapi.code-snippets vendored Normal file
View File

@ -0,0 +1,29 @@
{
// Place your FastGPT 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
// Placeholders with the same ids are connected.
// Example:
"Next api template": {
"scope": "javascript,typescript",
"prefix": "nextapi",
"body": [
"import type { NextApiRequest, NextApiResponse } from 'next';",
"import { NextAPI } from '@/service/middle/entry';",
"",
"type Props = {};",
"",
"type Response = {};",
"",
"async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<{}> {",
" $1",
" return {}",
"}",
"",
"export default NextAPI(handler);"
],
"description": "FastGPT Next API template"
}
}

View File

@ -13,7 +13,10 @@ images: []
1. `docker ps -a` 查看所有容器运行状态,检查是否全部 running如有异常尝试`docker logs 容器名`查看对应日志。
2. 容器都运行正常的,`docker logs 容器名` 查看报错日志
3. 无法解决时,可以找找[Issue](https://github.com/labring/FastGPT/issues),或新提 Issue私有部署错误务必提供详细的日志否则很难排查。
3. 带有`requestId`的,都是 OneAPI 提示错误,大部分都是因为模型接口报错。
4. 无法解决时,可以找找[Issue](https://github.com/labring/FastGPT/issues),或新提 Issue私有部署错误务必提供详细的日志否则很难排查。
## 二、通用问题
@ -90,4 +93,9 @@ FastGPT 模型配置文件中的 model 必须与 OneAPI 渠道中的模型对应
OneAPI 的 API Key 配置错误,需要修改`OPENAI_API_KEY`环境变量,并重启容器(先 docker-compose down 然后再 docker-compose up -d 运行一次)。
可以`exec`进入容器,`env`查看环境变量是否生效。
可以`exec`进入容器,`env`查看环境变量是否生效。
### bad_response_status_code bad response status code 503
1. 模型服务不可用
2. ....

View File

@ -4,6 +4,7 @@ import cronParser from 'cron-parser';
export const formatTime2YMDHM = (time?: Date) =>
time ? dayjs(time).format('YYYY-MM-DD HH:mm') : '';
export const formatTime2YMD = (time?: Date) => (time ? dayjs(time).format('YYYY-MM-DD') : '');
export const formatTime2HM = (time: Date = new Date()) => dayjs(time).format('HH:mm');
/* cron time parse */
export const cronParser2Fields = (cronString: string) => {

View File

@ -6,7 +6,7 @@ import { VariableInputEnum } from '../workflow/constants';
import { SelectedDatasetType } from '../workflow/api';
import { DatasetSearchModeEnum } from '../dataset/constants';
import { TeamTagSchema as TeamTagsSchemaType } from '@fastgpt/global/support/user/team/type.d';
import { StoreEdgeItemType } from 'core/workflow/type/edge';
import { StoreEdgeItemType } from '../workflow/type/edge';
export interface AppSchema {
_id: string;
@ -18,6 +18,7 @@ export interface AppSchema {
avatar: string;
intro: string;
updateTime: number;
modules: StoreNodeItemType[];
edges: StoreEdgeItemType[];

9
packages/global/core/app/version.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
import { StoreNodeItemType } from '../workflow/type';
import { StoreEdgeItemType } from '../workflow/type/edge';
export type AppVersionSchemaType = {
appId: string;
time: Date;
nodes: StoreNodeItemType[];
edges: StoreEdgeItemType[];
};

View File

@ -14,18 +14,21 @@ export enum WorkflowIOValueTypeEnum {
string = 'string',
number = 'number',
boolean = 'boolean',
object = 'object',
arrayString = 'arrayString',
arrayNumber = 'arrayNumber',
arrayBoolean = 'arrayBoolean',
arrayObject = 'arrayObject',
any = 'any',
chatHistory = 'chatHistory',
datasetQuote = 'datasetQuote',
dynamic = 'dynamic',
// plugin special type
selectApp = 'selectApp',
selectDataset = 'selectDataset',
// tool
tools = 'tools'
selectDataset = 'selectDataset'
}
/* reg: modulename key */
@ -173,3 +176,5 @@ export enum RuntimeEdgeStatusEnum {
'active' = 'active',
'skipped' = 'skipped'
}
export const VARIABLE_NODE_ID = 'VARIABLE_NODE_ID';

View File

@ -4,7 +4,7 @@ import { FlowNodeTypeEnum } from '../node/constant';
import { StoreNodeItemType } from '../type';
import { StoreEdgeItemType } from '../type/edge';
import { RuntimeEdgeItemType, RuntimeNodeItemType } from './type';
import { VARIABLE_NODE_ID } from '../../../../../projects/app/src/web/core/workflow/constants/index';
import { VARIABLE_NODE_ID } from '../constants';
export const initWorkflowEdgeStatus = (edges: StoreEdgeItemType[]): RuntimeEdgeItemType[] => {
return (

View File

@ -10,16 +10,26 @@ import {
NodeOutputKeyEnum,
FlowNodeTemplateTypeEnum
} from '../../constants';
import { Input_Template_Dataset_Quote } from '../input';
import { getNanoid } from '../../../../common/string/tools';
import { getHandleConfig } from '../utils';
import { FlowNodeInputItemType } from '../../type/io.d';
export const getOneQuoteInputTemplate = (key = getNanoid()): FlowNodeInputItemType => ({
...Input_Template_Dataset_Quote,
const defaultQuoteKey = 'defaultQuoteKey';
export const getOneQuoteInputTemplate = ({
key = getNanoid(),
index
}: {
key?: string;
index: number;
}): FlowNodeInputItemType => ({
key,
renderTypeList: [FlowNodeInputTypeEnum.custom],
description: ''
renderTypeList: [FlowNodeInputTypeEnum.reference],
label: `引用${index}`,
debugLabel: '知识库引用',
canEdit: key !== defaultQuoteKey,
description: '',
valueType: WorkflowIOValueTypeEnum.datasetQuote
});
export const DatasetConcatModule: FlowNodeTemplateType = {
@ -37,7 +47,7 @@ export const DatasetConcatModule: FlowNodeTemplateType = {
key: NodeInputKeyEnum.datasetMaxTokens,
renderTypeList: [FlowNodeInputTypeEnum.custom],
label: '最大 Tokens',
value: 1500,
value: 3000,
valueType: WorkflowIOValueTypeEnum.number
},
{
@ -45,7 +55,7 @@ export const DatasetConcatModule: FlowNodeTemplateType = {
renderTypeList: [FlowNodeInputTypeEnum.custom],
label: ''
},
getOneQuoteInputTemplate()
getOneQuoteInputTemplate({ key: defaultQuoteKey, index: 1 })
],
outputs: [
{

View File

@ -1,19 +1,12 @@
import type { NextApiResponse, NextApiHandler, NextApiRequest } from 'next';
import type { NextApiResponse, NextApiRequest } from 'next';
import NextCors from 'nextjs-cors';
export function withNextCors(handler: NextApiHandler): NextApiHandler {
return async function nextApiHandlerWrappedWithNextCors(
req: NextApiRequest,
res: NextApiResponse
) {
const methods = ['GET', 'eHEAD', 'PUT', 'PATCH', 'POST', 'DELETE'];
const origin = req.headers.origin;
await NextCors(req, res, {
methods,
origin: origin,
optionsSuccessStatus: 200
});
return handler(req, res);
};
export async function withNextCors(req: NextApiRequest, res: NextApiResponse) {
const methods = ['GET', 'eHEAD', 'PUT', 'PATCH', 'POST', 'DELETE'];
const origin = req.headers.origin;
await NextCors(req, res, {
methods,
origin: origin,
optionsSuccessStatus: 200
});
}

View File

@ -18,9 +18,10 @@ export const jsonRes = <T = any>(
message?: string;
data?: T;
error?: any;
url?: string;
}
) => {
const { code = 200, message = '', data = null, error } = props || {};
const { code = 200, message = '', data = null, error, url } = props || {};
const errResponseKey = typeof error === 'string' ? error : error?.message;
// Specified error
@ -47,7 +48,7 @@ export const jsonRes = <T = any>(
msg = error?.error?.message;
}
addLog.error(`response error: ${msg}`, error);
addLog.error(`Api response error: ${url}, ${msg}`, error);
}
res.status(code).json({

View File

@ -169,7 +169,16 @@ class PgClass {
}
async query<T extends QueryResultRow = any>(sql: string) {
const pg = await connectPg();
return pg.query<T>(sql);
const start = Date.now();
return pg.query<T>(sql).then((res) => {
const time = Date.now() - start;
if (time > 300) {
addLog.warn(`pg query time: ${time}ms, sql: ${sql}`);
}
return res;
});
}
}

View File

@ -0,0 +1,65 @@
import { AppSchema } from '@fastgpt/global/core/app/type';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { getLLMModel } from '../ai/model';
import { MongoAppVersion } from './versionSchema';
export const beforeUpdateAppFormat = <T extends AppSchema['modules'] | undefined>({
nodes
}: {
nodes: T;
}) => {
if (nodes) {
let maxTokens = 3000;
nodes.forEach((item) => {
if (
item.flowNodeType === FlowNodeTypeEnum.chatNode ||
item.flowNodeType === FlowNodeTypeEnum.tools
) {
const model =
item.inputs.find((item) => item.key === NodeInputKeyEnum.aiModel)?.value || '';
const chatModel = getLLMModel(model);
const quoteMaxToken = chatModel.quoteMaxToken || 3000;
maxTokens = Math.max(maxTokens, quoteMaxToken);
}
});
nodes.forEach((item) => {
if (item.flowNodeType === FlowNodeTypeEnum.datasetSearchNode) {
item.inputs.forEach((input) => {
if (input.key === NodeInputKeyEnum.datasetMaxTokens) {
const val = input.value as number;
if (val > maxTokens) {
input.value = maxTokens;
}
}
});
}
});
}
return {
nodes
};
};
export const getAppLatestVersion = async (appId: string, app?: AppSchema) => {
const version = await MongoAppVersion.findOne({
appId
}).sort({
time: -1
});
if (version) {
return {
nodes: version.nodes,
edges: version.edges
};
}
return {
nodes: app?.modules || [],
edges: app?.edges || []
};
};

View File

@ -8,7 +8,7 @@ import {
TeamMemberCollectionName
} from '@fastgpt/global/support/user/team/constant';
export const appCollectionName = 'apps';
export const AppCollectionName = 'apps';
const AppSchema = new Schema({
teamId: {
@ -46,6 +46,8 @@ const AppSchema = new Schema({
type: Date,
default: () => new Date()
},
// tmp store
modules: {
type: Array,
default: []
@ -92,6 +94,6 @@ try {
}
export const MongoApp: Model<AppType> =
models[appCollectionName] || model(appCollectionName, AppSchema);
models[AppCollectionName] || model(AppCollectionName, AppSchema);
MongoApp.syncIndexes();

View File

@ -0,0 +1,36 @@
import { connectionMongo, type Model } from '../../common/mongo';
const { Schema, model, models } = connectionMongo;
import { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
export const AppVersionCollectionName = 'app.versions';
const AppVersionSchema = new Schema({
appId: {
type: Schema.Types.ObjectId,
ref: AppVersionCollectionName,
required: true
},
time: {
type: Date,
default: () => new Date()
},
nodes: {
type: Array,
default: []
},
edges: {
type: Array,
default: []
}
});
try {
AppVersionSchema.index({ appId: 1, time: -1 });
} catch (error) {
console.log(error);
}
export const MongoAppVersion: Model<AppVersionSchemaType> =
models[AppVersionCollectionName] || model(AppVersionCollectionName, AppVersionSchema);
MongoAppVersion.syncIndexes();

View File

@ -7,7 +7,7 @@ import {
TeamCollectionName,
TeamMemberCollectionName
} from '@fastgpt/global/support/user/team/constant';
import { appCollectionName } from '../app/schema';
import { AppCollectionName } from '../app/schema';
import { userCollectionName } from '../../support/user/schema';
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
@ -40,7 +40,7 @@ const ChatItemSchema = new Schema({
},
appId: {
type: Schema.Types.ObjectId,
ref: appCollectionName,
ref: AppCollectionName,
required: true
},
time: {

View File

@ -6,7 +6,7 @@ import {
TeamCollectionName,
TeamMemberCollectionName
} from '@fastgpt/global/support/user/team/constant';
import { appCollectionName } from '../app/schema';
import { AppCollectionName } from '../app/schema';
export const chatCollectionName = 'chat';
@ -31,7 +31,7 @@ const ChatSchema = new Schema({
},
appId: {
type: Schema.Types.ObjectId,
ref: appCollectionName,
ref: AppCollectionName,
required: true
},
updateTime: {

View File

@ -220,9 +220,16 @@ export async function dispatchWorkFlow({
).then((result) => {
const flat = result.flat();
if (flat.length === 0) return;
// update output
// Update the node output at the end of the run and get the next nodes
const nextNodes = flat.map((item) => nodeOutput(item.node, item.result)).flat();
return checkNodeCanRun(nextNodes);
// Remove repeat nodes(Make sure that the node is only executed once)
const filterNextNodes = nextNodes.filter(
(node, index, self) => self.findIndex((t) => t.nodeId === node.nodeId) === index
);
return checkNodeCanRun(filterNextNodes);
});
}
// 运行完一轮后,清除连线的状态,避免污染进程

View File

@ -8,6 +8,7 @@ import {
} from '@fastgpt/global/core/workflow/template/system/ifElse/type';
import { ModuleDispatchProps } from '@fastgpt/global/core/workflow/type';
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
import { getReferenceVariableValue } from '@fastgpt/global/core/workflow/runtime/utils';
type Props = ModuleDispatchProps<{
[NodeInputKeyEnum.condition]: IfElseConditionType;
@ -20,20 +21,21 @@ function checkCondition(condition: VariableConditionEnum, variableValue: any, va
[VariableConditionEnum.isNotEmpty]: () => !!variableValue,
[VariableConditionEnum.equalTo]: () => variableValue === value,
[VariableConditionEnum.notEqual]: () => variableValue !== value,
[VariableConditionEnum.greaterThan]: () => variableValue > Number(value),
[VariableConditionEnum.lessThan]: () => variableValue < Number(value),
[VariableConditionEnum.greaterThanOrEqualTo]: () => variableValue >= Number(value),
[VariableConditionEnum.lessThanOrEqualTo]: () => variableValue <= Number(value),
[VariableConditionEnum.include]: () => variableValue.includes(value),
[VariableConditionEnum.notInclude]: () => !variableValue.includes(value),
[VariableConditionEnum.startWith]: () => variableValue.startsWith(value),
[VariableConditionEnum.endWith]: () => variableValue.endsWith(value),
[VariableConditionEnum.lengthEqualTo]: () => variableValue.length === Number(value),
[VariableConditionEnum.lengthNotEqualTo]: () => variableValue.length !== Number(value),
[VariableConditionEnum.lengthGreaterThan]: () => variableValue.length > Number(value),
[VariableConditionEnum.lengthGreaterThanOrEqualTo]: () => variableValue.length >= Number(value),
[VariableConditionEnum.lengthLessThan]: () => variableValue.length < Number(value),
[VariableConditionEnum.lengthLessThanOrEqualTo]: () => variableValue.length <= Number(value)
[VariableConditionEnum.greaterThan]: () => Number(variableValue) > Number(value),
[VariableConditionEnum.lessThan]: () => Number(variableValue) < Number(value),
[VariableConditionEnum.greaterThanOrEqualTo]: () => Number(variableValue) >= Number(value),
[VariableConditionEnum.lessThanOrEqualTo]: () => Number(variableValue) <= Number(value),
[VariableConditionEnum.include]: () => variableValue?.includes(value),
[VariableConditionEnum.notInclude]: () => !variableValue?.includes(value),
[VariableConditionEnum.startWith]: () => variableValue?.startsWith(value),
[VariableConditionEnum.endWith]: () => variableValue?.endsWith(value),
[VariableConditionEnum.lengthEqualTo]: () => variableValue?.length === Number(value),
[VariableConditionEnum.lengthNotEqualTo]: () => variableValue?.length !== Number(value),
[VariableConditionEnum.lengthGreaterThan]: () => variableValue?.length > Number(value),
[VariableConditionEnum.lengthGreaterThanOrEqualTo]: () =>
variableValue?.length >= Number(value),
[VariableConditionEnum.lengthLessThan]: () => variableValue?.length < Number(value),
[VariableConditionEnum.lengthLessThanOrEqualTo]: () => variableValue?.length <= Number(value)
};
return (operations[condition] || (() => false))();
@ -43,15 +45,18 @@ export const dispatchIfElse = async (props: Props): Promise<DispatchNodeResultTy
const {
params,
runtimeNodes,
variables,
node: { nodeId }
} = props;
const { condition, ifElseList } = params;
const listResult = ifElseList.map((item) => {
const { variable, condition: variableCondition, value } = item;
const variableValue = runtimeNodes
.find((node) => node.nodeId === variable[0])
?.outputs?.find((item) => item.id === variable[1])?.value;
const variableValue = getReferenceVariableValue({
value: variable,
variables,
nodes: runtimeNodes
});
return checkCondition(variableCondition as VariableConditionEnum, variableValue, value || '');
});

View File

@ -6,7 +6,7 @@ import {
TeamCollectionName,
TeamMemberCollectionName
} from '@fastgpt/global/support/user/team/constant';
import { appCollectionName } from '../../core/app/schema';
import { AppCollectionName } from '../../core/app/schema';
const OutLinkSchema = new Schema({
shareId: {
@ -25,7 +25,7 @@ const OutLinkSchema = new Schema({
},
appId: {
type: Schema.Types.ObjectId,
ref: appCollectionName,
ref: AppCollectionName,
required: true
},
type: {

View File

@ -30,12 +30,10 @@ export async function authApp({
// get app
const app = await MongoApp.findOne({ _id: appId, teamId }).lean();
if (!app) {
return Promise.reject(AppErrEnum.unAuthApp);
return Promise.reject(AppErrEnum.unExist);
}
const isOwner =
role !== TeamMemberRoleEnum.visitor &&
(String(app.tmbId) === tmbId || role === TeamMemberRoleEnum.owner);
const isOwner = String(app.tmbId) === tmbId;
const canWrite =
isOwner ||
(app.permission === PermissionTypeEnum.public && role !== TeamMemberRoleEnum.visitor);

View File

@ -40,6 +40,7 @@ export const iconPaths = {
'common/paramsLight': () => import('./icons/common/paramsLight.svg'),
'common/playFill': () => import('./icons/common/playFill.svg'),
'common/playLight': () => import('./icons/common/playLight.svg'),
'common/publishFill': () => import('./icons/common/publishFill.svg'),
'common/questionLight': () => import('./icons/common/questionLight.svg'),
'common/refreshLight': () => import('./icons/common/refreshLight.svg'),
'common/resultLight': () => import('./icons/common/resultLight.svg'),

View File

@ -0,0 +1,6 @@
<svg t="1714186779322" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1213"
width="128" height="128">
<path
d="M957.78379975 134.65795581c-2.56331135 83.17185735-20.44423515 163.12467052-50.30317593 240.57510884-35.69958418 92.67796903-87.96862781 174.24030494-161.20251807 241.63355181-5.94396957 5.46839892-7.80783541 11.32361235-7.01830818 19.026796 2.5328431 24.92705029 4.5596486 49.91503711 7.12295994 74.84208611 5.9598665 58.12425835-14.32010468 105.71575589-61.01609761 139.47861502-50.06340414 36.20694738-102.1695094 69.6412786-153.84243498 103.55515469-32.57194606 21.38212813-72.60730269 1.98176462-76.2277327-36.80306673-4.17150847-44.53539301-7.45016355-89.16219079-10.5340864-133.78633995-1.53533793-22.33459502-0.93921859-22.52932723-22.48428636-25.38937392-57.18636407-7.56938794-101.94563325-34.8213021-132.68418171-84.27401429-15.81040175-25.46488176-23.6473811-53.61097493-26.40410034-83.18642866-0.81866989-8.86495278-4.3808133-11.13020574-12.70925895-11.59252939-46.28003447-2.57788397-92.52960201-5.3332789-138.74869996-8.8199132-38.44173211-2.92098321-57.93085045-43.61339568-37.32367695-77.56966271 14.4671474-23.86993165 29.87483771-47.17288918 44.81888136-70.76065808 16.1071371-25.38937393 32.07915418-50.85425569 48.21675953-76.21316007 29.53173846-46.41383019 72.1012638-71.48924752 126.5905208-71.93699984 31.2008721-0.23844748 62.46003078 5.27366671 93.64633157 8.64107663 5.89892998 0.64115892 10.0863341-0.16426396 14.16908774-4.73848387 72.39799786-81.24970362 162.00661664-136.08206117 263.446211-172.76457792 57.33473241-20.74096921 116.33859714-34.28479495 176.89237242-40.54271983 30.51467231-3.17400331 61.23864946-4.32120112 91.72285352 1.34060572 16.61450031 3.08392285 17.46363973 3.77012135 20.02562678 20.05609502C956.4869093 101.71509088 958.51238919 118.07524806 957.78379975 134.65795581L957.78379975 134.65795581zM813.20902898 339.81696881c0.87828208-71.05871775-57.73744384-132.1635715-127.410515-133.309445-72.35428259-1.20680999-135.02361821 51.61331338-136.73779142 134.08439962-1.44525748 68.77756914 60.61338619 131.07466028 129.88374722 131.55155523C752.69897026 472.67866149 812.29895435 413.94371248 813.20902898 339.81696881L813.20902898 339.81696881zM195.18722288 640.09538918c13.69219278 0.17883529 21.29204896 6.24335355 26.19479682 20.29321819 19.25067215 55.04033549 54.28127779 96.80443794 105.06002435 125.11479508 11.20438926 6.24335355 23.0207935 11.30904103 35.22401354 15.48187512 21.93320788 7.50845143 26.93928317 28.96476307 10.66788212 45.47328728-24.10705481 24.43558273-48.46977833 48.60357404-72.56226181 73.02590976-7.97209938 8.07542683-16.74829602 11.90383476-27.95268527 7.71775497-11.38322454-4.2470176-16.88209172-12.72383026-17.46363974-24.55480712-0.35767186-7.49520313-0.17883529-15.03544713-0.35767188-22.55846986-0.31263099-13.81274148-0.44775102-13.91739325-13.11197036-9.71541653-27.08765021 9.01464541-54.11701383 18.10347435-81.20466405 27.10354714-7.9866707 2.66796441-16.00248532 3.77012135-23.81032074-0.67030285-11.44416105-6.51226928-15.76403785-18.14851392-11.160674-32.46729429 8.99874849-27.93811395 18.43200097-55.71196266 27.64005428-83.57456879 3.99267318-12.09856827 3.7396531-12.35158706-9.26766421-12.55956629-7.83830367-0.11922438-15.70442566 0.01457133-23.54140371-0.37224318-11.4136928-0.55107846-19.5341592-6.09498652-23.75070856-16.94170392-4.14104022-10.56455466-1.49029705-19.77260668 6.19831398-27.54997384 25.15092515-25.40526957 50.42240031-50.68999172 75.6620816-76.00518086C182.68726879 642.31560258 188.64713398 639.45423026 195.18722288 640.09538918L195.18722288 640.09538918z"
p-id="1214"></path>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -54,21 +54,20 @@ const MultipleRowSelect = ({
bg: 'primary.50',
color: 'primary.600'
}}
onClick={() => {
const newValue = [...cloneValue];
newValue[index] = item.value;
setCloneValue(newValue);
if (!hasChildren) {
onSelect(newValue);
onClose();
}
}}
{...(item.value === selectedValue
? {
color: 'primary.600'
}
: {
onClick: () => {
const newValue = [...cloneValue];
newValue[index] = item.value;
setCloneValue(newValue);
if (!hasChildren) {
onSelect(newValue);
onClose();
}
}
})}
: {})}
>
{item.label}
</Flex>

View File

@ -12,7 +12,7 @@ const MyTooltip = ({ children, forceShow = false, shouldWrapChildren = true, ...
<Tooltip
className="tooltip"
bg={'white'}
arrowShadowColor={' rgba(0,0,0,0.05)'}
arrowShadowColor={'rgba(0,0,0,0.05)'}
hasArrow
arrowSize={12}
offset={[-15, 15]}

View File

@ -0,0 +1,24 @@
import { useTranslation } from 'next-i18next';
import { useEffect } from 'react';
export const useBeforeunload = (props?: { callback?: () => any; tip?: string }) => {
const { t } = useTranslation('common');
const { tip = t('Confirm to leave the page'), callback } = props || {};
useEffect(() => {
const listen =
process.env.NODE_ENV !== 'production'
? (e: any) => {
e.preventDefault();
e.returnValue = tip;
callback?.();
}
: () => {};
window.addEventListener('beforeunload', listen);
return () => {
window.removeEventListener('beforeunload', listen);
};
}, [tip, callback]);
};

View File

@ -59,7 +59,7 @@ export const useConfirm = (props?: {
onClose,
ConfirmModal: useCallback(
({
closeText = t('common.Close'),
closeText = t('common.Cancel'),
confirmText = t('common.Confirm'),
isLoading,
bg,

View File

@ -356,9 +356,6 @@ importers:
'@fortaine/fetch-event-source':
specifier: ^3.0.6
version: 3.0.6
'@node-rs/jieba':
specifier: 1.10.0
version: 1.10.0
'@tanstack/react-query':
specifier: ^4.24.10
version: 4.24.10(react-dom@18.2.0)(react@18.2.0)

View File

@ -35,6 +35,7 @@ const nextConfig = {
if (isServer) {
config.externals.push('isolated-vm');
config.externals.push('worker_threads');
config.externals.push('@node-rs/jieba');
if (config.name === 'server') {
// config.output.globalObject = 'self';

View File

@ -23,7 +23,6 @@
"@fastgpt/service": "workspace:*",
"@fastgpt/web": "workspace:*",
"@fortaine/fetch-event-source": "^3.0.6",
"@node-rs/jieba": "1.10.0",
"@tanstack/react-query": "^4.24.10",
"@types/nprogress": "^0.2.0",
"axios": "^1.5.1",

View File

@ -73,6 +73,7 @@
"Confirm Import": "Import",
"Confirm Move": "Move here",
"Confirm Update": "Update",
"Confirm to leave the page": "Are you sure to leave this page?",
"Copy": "Copy",
"Copy Successful": "Copy Successful",
"Course": "",
@ -278,6 +279,7 @@
"Api request desc": "Access to the existing system through API, or enterprise micro, flying book, etc",
"App intro": "App intro",
"App params config": "App Config",
"Auto Save time": "Auto-saved: {{time}}",
"Chat Variable": "",
"Config schedule plan": "Config schedule config",
"Config whisper": "Config whisper",
@ -289,6 +291,11 @@
"Max histories": "Dialog round",
"Max tokens": "Max tokens",
"Name and avatar": "Avatar & Name",
"Onclick to save": "Save",
"Publish": "Publish",
"Publish Confirm": "Sure to release the app? App status is immediately updated across all publishing channels.",
"Publish Failed": "Publish error",
"Publish Success": "Publish Success",
"Question Guide": "Question Guide",
"Question Guide Tip": "At the end of the conversation, three leading questions will be asked.",
"Quote prompt": "Quote prompt",
@ -1035,11 +1042,16 @@
},
"valueType": {
"any": "Any",
"arrayBoolean": "Array Boolean",
"arrayNumber": "Array Number",
"arrayObject": "Array Object",
"arrayString": "Array String",
"boolean": "Boolean",
"chatHistory": "History",
"datasetQuote": "Dataset Quote",
"dynamicTargetInput": "Dynamic Input",
"number": "Number",
"object": "Object",
"selectApp": "Select App",
"selectDataset": "Select Dataset",
"string": "String",
@ -1128,6 +1140,7 @@
"textarea": "Textarea"
},
"tool": {
"Handle": "Tool handle",
"Select Tool": "Select Tool"
}
}

View File

@ -73,6 +73,7 @@
"Confirm Import": "确认导入",
"Confirm Move": "移动到这",
"Confirm Update": "确认更新",
"Confirm to leave the page": "确认离开该页面?",
"Copy": "复制",
"Copy Successful": "复制成功",
"Course": "",
@ -278,6 +279,7 @@
"Api request desc": "通过 API 接入到已有系统中,或企微、飞书等",
"App intro": "应用介绍",
"App params config": "应用配置",
"Auto Save time": "自动保存: {{time}}",
"Chat Variable": "对话框变量",
"Config schedule plan": "配置定时执行",
"Config whisper": "配置语音输入",
@ -289,6 +291,11 @@
"Max histories": "聊天记录数量",
"Max tokens": "回复上限",
"Name and avatar": "头像 & 名称",
"Onclick to save": "点击保存",
"Publish": "发布",
"Publish Confirm": "确认发布应用?会立即更新所有发布渠道的应用状态。",
"Publish Failed": "发布失败",
"Publish Success": "发布成功",
"Question Guide": "猜你想问",
"Question Guide Tip": "对话结束后,会为生成 3 个引导性问题。",
"Quote prompt": "引用模板提示词",
@ -689,7 +696,7 @@
"Data file progress": "数据上传进度",
"Data process params": "数据处理参数",
"Down load csv template": "点击下载 CSV 模板",
"Embedding Estimated Price Tips": "仅使用索引模型,消耗少量Tokens: {{price}}积分/1k Tokens",
"Embedding Estimated Price Tips": "仅使用索引模型,消耗少量AI积分: {{price}}积分/1k Tokens",
"Estimated Price": "预估价格: {{amount}}{{unit}}",
"Estimated Price Tips": "QA计费为\n输入: {{charsPointsPrice}}积分/1k Tokens",
"Estimated points": "预估消耗 {{points}} 积分",
@ -716,7 +723,7 @@
"Preview chunks": "预览分段最多5段",
"Preview raw text": "预览源文本最多3000字",
"Process way": "处理方式",
"QA Estimated Price Tips": "需调用文件处理模型,需要消耗较多Tokens: {{price}}积分/1k Tokens",
"QA Estimated Price Tips": "需调用文件处理模型,需要消耗较多AI积分: {{price}}积分/1k Tokens",
"QA Import": "QA拆分",
"QA Import Tip": "根据一定规则,将文本拆成一段较大的段落,调用 AI 为该段落生成问答对。有非常高的检索精度,但是会丢失很多内容细节。",
"Re Preview": "重新生成预览",
@ -1036,11 +1043,16 @@
},
"valueType": {
"any": "任意",
"arrayBoolean": "布尔数组",
"arrayNumber": "数字数组",
"arrayObject": "对象数组",
"arrayString": "字符串数组",
"boolean": "布尔",
"chatHistory": "聊天记录",
"datasetQuote": "引用内容",
"chatHistory": "历史记录",
"datasetQuote": "知识库类型",
"dynamicTargetInput": "动态字段输入",
"number": "数字",
"object": "对象",
"selectApp": "应用选择",
"selectDataset": "知识库选择",
"string": "字符串",
@ -1129,6 +1141,7 @@
"textarea": "多行输入框"
},
"tool": {
"Handle": "工具连接器",
"Select Tool": "选择工具"
}
}
@ -1421,7 +1434,7 @@
"user": {
"AI point standard": "AI积分标准",
"Avatar": "头像",
"Go laf env": "点击前往 laf 获取 PAT 凭证。",
"Go laf env": "点击前往 {{env}} 获取 PAT 凭证。",
"Laf account course": "查看绑定 laf 账号教程。",
"Laf account intro": "绑定你的laf账号后你将可以在工作流中使用 laf 模块,实现在线编写代码。",
"Need to login": "请先登录",

View File

@ -36,6 +36,7 @@ import { postWorkflowDebug } from '@/web/core/workflow/api';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { checkNodeRunStatus } from '@fastgpt/global/core/workflow/runtime/utils';
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
import { delay } from '@fastgpt/global/common/system/utils';
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
@ -83,7 +84,7 @@ export type useFlowProviderStoreType = {
sourceHandle?: string | undefined;
targetHandle?: string | undefined;
}) => void;
initData: (e: { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] }) => void;
initData: (e: { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] }) => Promise<void>;
splitToolInputs: (
inputs: FlowNodeInputItemType[],
nodeId: string
@ -147,7 +148,10 @@ const StateContext = createContext<useFlowProviderStoreType>({
hasToolNode: false,
connectingEdge: undefined,
basicNodeTemplates: [],
initData: function (e: { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] }): void {
initData: function (e: {
nodes: StoreNodeItemType[];
edges: StoreEdgeItemType[];
}): Promise<void> {
throw new Error('Function not implemented.');
},
hoverNodeId: undefined,
@ -608,16 +612,14 @@ export const FlowProvider = ({
);
const initData = useCallback(
(e: { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] }) => {
async (e: { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] }) => {
setNodes(e.nodes?.map((item) => storeNode2FlowNode({ item })));
setEdges(e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })));
setTimeout(() => {
onFixView();
}, 100);
await delay(200);
},
[setEdges, setNodes, onFixView]
[setEdges, setNodes]
);
useEffect(() => {

View File

@ -10,13 +10,11 @@ import { SmallAddIcon } from '@chakra-ui/icons';
import { WorkflowIOValueTypeEnum, NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { getOneQuoteInputTemplate } from '@fastgpt/global/core/workflow/template/system/datasetConcat';
import { useFlowProviderStore } from '../FlowProvider';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MySlider from '@/components/Slider';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import RenderOutput from './render/RenderOutput';
import Reference from './render/RenderInput/templates/Reference';
import IOTitle from '../components/IOTitle';
const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
@ -47,46 +45,13 @@ const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
return maxTokens;
}, [llmModelList, nodeList]);
const RenderQuoteList = useMemo(() => {
return (
<Box mt={-2}>
{quotes.map((quote, i) => (
<Box key={quote.key} _notLast={{ mb: 4 }}>
<Flex alignItems={'center'}>
<Box fontWeight={'medium'} color={'myGray.600'}>
{t('core.chat.Quote')}
{i + 1}
</Box>
<MyIcon
ml={2}
w={'14px'}
name={'delete'}
cursor={'pointer'}
color={'myGray.600'}
_hover={{ color: 'red.600' }}
onClick={() => {
onChangeNode({
nodeId,
type: 'delInput',
key: quote.key
});
}}
/>
</Flex>
<Reference nodeId={nodeId} item={quote} />
</Box>
))}
</Box>
);
}, [nodeId, onChangeNode, quotes, t]);
const onAddField = useCallback(() => {
onChangeNode({
nodeId,
type: 'addInput',
value: getOneQuoteInputTemplate()
value: getOneQuoteInputTemplate({ index: quotes.length + 1 })
});
}, [nodeId, onChangeNode]);
}, [nodeId, onChangeNode, quotes.length]);
const CustomComponent = useMemo(() => {
return {
@ -141,7 +106,7 @@ const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
<NodeCard minW={'400px'} selected={selected} {...data}>
<Container position={'relative'}>
<RenderInput nodeId={nodeId} flowInputList={inputs} CustomComponent={CustomComponent} />
{RenderQuoteList}
{/* {RenderQuoteList} */}
</Container>
<Container>
<IOTitle text={t('common.Output')} />

View File

@ -123,7 +123,7 @@ const NodeIfElse = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: 'condition',
key: NodeInputKeyEnum.condition,
value: {
...conditionInput,
value: conditionInput.value === 'OR' ? 'AND' : 'OR'
@ -297,7 +297,11 @@ const ConditionSelect = ({
valueType === WorkflowIOValueTypeEnum.datasetQuote ||
valueType === WorkflowIOValueTypeEnum.dynamic ||
valueType === WorkflowIOValueTypeEnum.selectApp ||
valueType === WorkflowIOValueTypeEnum.tools
valueType === WorkflowIOValueTypeEnum.arrayBoolean ||
valueType === WorkflowIOValueTypeEnum.arrayNumber ||
valueType === WorkflowIOValueTypeEnum.arrayObject ||
valueType === WorkflowIOValueTypeEnum.arrayString ||
valueType === WorkflowIOValueTypeEnum.object
)
return arrayConditionList;

View File

@ -5,21 +5,14 @@ import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import dynamic from 'next/dynamic';
import { Box, Button, Flex } from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import {
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import Container from '../components/Container';
import { EditInputFieldMapType, EditNodeFieldType } from '@fastgpt/global/core/workflow/node/type';
import {
FlowNodeInputItemType,
FlowNodeOutputItemType
} from '@fastgpt/global/core/workflow/type/io';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';
import { useFlowProviderStore } from '../FlowProvider';
import RenderInput from './render/RenderInput';
import { getNanoid } from '@fastgpt/global/common/string/tools';
const FieldEditModal = dynamic(() => import('./render/FieldEditModal'));
@ -37,7 +30,7 @@ const createEditField: EditInputFieldMapType = {
const NodePluginOutput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs, outputs } = data;
const { nodeId, inputs } = data;
const { onChangeNode } = useFlowProviderStore();
const [createField, setCreateField] = useState<EditNodeFieldType>();
@ -93,13 +86,6 @@ const NodePluginOutput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
canEdit: true,
editField: createEditField
};
const newOutput: FlowNodeOutputItemType = {
id: getNanoid(),
key: data.key,
valueType: data.valueType,
label: data.label,
type: FlowNodeOutputTypeEnum.static
};
onChangeNode({
nodeId,

View File

@ -22,7 +22,7 @@ const NodeStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
}}
{...data}
>
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'} textAlign={'end'}>
<Container>
<IOTitle text={t('common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>

View File

@ -149,13 +149,6 @@ const FieldEditModal = ({
[showDynamicInputSelect, t]
);
const dataTypeSelectList = Object.values(FlowValueTypeMap)
.slice(0, -2)
.map((item) => ({
label: t(item.label),
value: item.value
}));
const { register, getValues, setValue, handleSubmit, watch } = useForm<EditNodeFieldType>({
defaultValues: {
...defaultValue,
@ -224,6 +217,13 @@ const FieldEditModal = ({
return inputType === FlowNodeInputTypeEnum.addInputParam;
}, [inputType]);
const slicedTypeMap = Object.values(FlowValueTypeMap).slice(0, -1);
const dataTypeSelectList = slicedTypeMap.map((item) => ({
label: t(item.label),
value: item.value
}));
const onSubmitSuccess = useCallback(
(data: EditNodeFieldType) => {
data.key = data?.key?.trim();
@ -270,7 +270,7 @@ const FieldEditModal = ({
changeKey: !keys.includes(data.key)
});
},
[defaultField.key, keys, onSubmit, showValueTypeSelect, t, toast]
[defaultField.key, inputTypeList, keys, onSubmit, showValueTypeSelect, t, toast]
);
const onSubmitError = useCallback(
(e: Object) => {

View File

@ -28,17 +28,9 @@ export const ToolTargetHandle = ({ nodeId }: ToolHandleProps) => {
(connectingEdge?.handleId !== NodeOutputKeyEnum.selectedTools ||
edges.some((edge) => edge.targetHandle === getHandleId(nodeId, 'target', 'top')));
const valueTypeMap = FlowValueTypeMap[WorkflowIOValueTypeEnum.tools];
const Render = useMemo(() => {
return (
<MyTooltip
label={t('app.module.type', {
type: t(valueTypeMap?.label),
description: valueTypeMap?.description
})}
shouldWrapChildren={false}
>
<MyTooltip label={t('core.workflow.tool.Handle')} shouldWrapChildren={false}>
<Handle
style={{
borderRadius: '0',
@ -63,7 +55,7 @@ export const ToolTargetHandle = ({ nodeId }: ToolHandleProps) => {
</Handle>
</MyTooltip>
);
}, [handleId, hidden, t, valueTypeMap?.description, valueTypeMap?.label]);
}, [handleId, hidden, t]);
return Render;
};
@ -72,8 +64,6 @@ export const ToolSourceHandle = ({ nodeId }: ToolHandleProps) => {
const { t } = useTranslation();
const { setEdges } = useFlowProviderStore();
const valueTypeMap = FlowValueTypeMap[WorkflowIOValueTypeEnum.tools];
/* onConnect edge, delete tool input and switch */
const onConnect = useCallback(
(e: Connection) => {
@ -90,13 +80,7 @@ export const ToolSourceHandle = ({ nodeId }: ToolHandleProps) => {
const Render = useMemo(() => {
return (
<MyTooltip
label={t('app.module.type', {
type: t(valueTypeMap?.label),
description: valueTypeMap?.description
})}
shouldWrapChildren={false}
>
<MyTooltip label={t('core.workflow.tool.Handle')} shouldWrapChildren={false}>
<Handle
style={{
borderRadius: '0',
@ -120,7 +104,7 @@ export const ToolSourceHandle = ({ nodeId }: ToolHandleProps) => {
</Handle>
</MyTooltip>
);
}, [onConnect, t, valueTypeMap?.description, valueTypeMap?.label]);
}, [onConnect, t]);
return Render;
};

View File

@ -513,11 +513,14 @@ const NodeDebugResponse = React.memo(function NodeDebugResponse({
{/* result */}
{debugResult.showResult && (
<Card
className="nowheel"
position={'absolute'}
right={'-430px'}
top={0}
zIndex={10}
w={'420px'}
maxH={'540px'}
overflowY={'auto'}
border={'base'}
>
{/* Status header */}

View File

@ -1,7 +1,4 @@
import {
FlowNodeInputItemType,
FlowNodeOutputItemType
} from '@fastgpt/global/core/workflow/type/io.d';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'next-i18next';
import { useFlowProviderStore } from '../../../FlowProvider';
@ -14,7 +11,6 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import dynamic from 'next/dynamic';
import { EditNodeFieldType } from '@fastgpt/global/core/workflow/node/type';
import { FlowValueTypeMap } from '@/web/core/workflow/constants/dataType';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import ValueTypeLabel from '../ValueTypeLabel';
const FieldEditModal = dynamic(() => import('../FieldEditModal'));
@ -37,16 +33,10 @@ const InputLabel = ({ nodeId, input }: Props) => {
renderTypeList,
valueType,
canEdit,
key,
value
key
} = input;
const [editField, setEditField] = useState<EditNodeFieldType>();
const valueTypeLabel = useMemo(
() => (valueType ? t(FlowValueTypeMap[valueType]?.label) : ''),
[t, valueType]
);
const onChangeRenderType = useCallback(
(e: string) => {
const index = renderTypeList.findIndex((item) => item === e) || 0;
@ -84,35 +74,35 @@ const InputLabel = ({ nodeId, input }: Props) => {
)}
</Box>
{/* value type */}
{renderType === FlowNodeInputTypeEnum.reference && !!valueTypeLabel && (
<ValueTypeLabel>{valueTypeLabel}</ValueTypeLabel>
)}
{renderType === FlowNodeInputTypeEnum.reference && <ValueTypeLabel valueType={valueType} />}
{/* edit config */}
{canEdit && (
<>
<MyIcon
name={'common/settingLight'}
w={'14px'}
cursor={'pointer'}
ml={3}
color={'myGray.600'}
_hover={{ color: 'primary.500' }}
onClick={() =>
setEditField({
inputType: renderTypeList[0],
valueType: valueType,
key,
label,
description,
isToolInput: !!toolDescription,
defaultValue: input.defaultValue,
maxLength: input.maxLength,
max: input.max,
min: input.min,
dynamicParamDefaultValue: input.dynamicParamDefaultValue
})
}
/>
{input.editField && Object.keys(input.editField).length > 0 && (
<MyIcon
name={'common/settingLight'}
w={'14px'}
cursor={'pointer'}
ml={3}
color={'myGray.600'}
_hover={{ color: 'primary.500' }}
onClick={() =>
setEditField({
inputType: renderTypeList[0],
valueType: valueType,
key,
label,
description,
isToolInput: !!toolDescription,
defaultValue: input.defaultValue,
maxLength: input.maxLength,
max: input.max,
min: input.min,
dynamicParamDefaultValue: input.dynamicParamDefaultValue
})
}
/>
)}
<MyIcon
className="delete"
name={'delete'}
@ -207,8 +197,7 @@ const InputLabel = ({ nodeId, input }: Props) => {
selectedTypeIndex,
t,
toolDescription,
valueType,
valueTypeLabel
valueType
]);
return RenderLabel;

View File

@ -20,7 +20,7 @@ import {
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
import PromptTemplate from '@/components/PromptTemplate';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Reference from './Reference';
import { getSystemVariables } from '@/web/core/app/utils';
@ -152,7 +152,7 @@ const SettingQuotePrompt = (props: RenderInputProps) => {
<Box position={'relative'} color={'myGray.600'} fontWeight={'medium'}>
{t('core.module.Dataset quote.label')}
</Box>
<ValueTypeLabel>{t('core.module.valueType.datasetQuote')}</ValueTypeLabel>
<ValueTypeLabel valueType={WorkflowIOValueTypeEnum.datasetQuote} />
<MyTooltip label={t('core.module.Setting quote prompt')}>
<MyIcon

View File

@ -6,7 +6,6 @@ import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/const
import { SourceHandle } from '../Handle';
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
import { Position } from 'reactflow';
import { FlowValueTypeMap } from '@/web/core/workflow/constants/dataType';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import ValueTypeLabel from '../ValueTypeLabel';
@ -14,11 +13,6 @@ const OutputLabel = ({ nodeId, output }: { nodeId: string; output: FlowNodeOutpu
const { t } = useTranslation();
const { label = '', description, valueType } = output;
const valueTypeLabel = useMemo(
() => (valueType ? t(FlowValueTypeMap[valueType]?.label) : '-'),
[t, valueType]
);
const Render = useMemo(() => {
return (
<Box position={'relative'}>
@ -34,11 +28,15 @@ const OutputLabel = ({ nodeId, output }: { nodeId: string; output: FlowNodeOutpu
}
: {})}
>
<Box position={'relative'} mr={1}>
<Box
position={'relative'}
mr={1}
ml={output.type === FlowNodeOutputTypeEnum.source ? 1 : 0}
>
{t(label)}
</Box>
{description && <QuestionTip label={t(description)} />}
<ValueTypeLabel>{valueTypeLabel}</ValueTypeLabel>
<ValueTypeLabel valueType={valueType} />
</Flex>
{output.type === FlowNodeOutputTypeEnum.source && (
<SourceHandle
@ -50,7 +48,7 @@ const OutputLabel = ({ nodeId, output }: { nodeId: string; output: FlowNodeOutpu
)}
</Box>
);
}, [description, output.key, output.type, label, nodeId, t, valueTypeLabel]);
}, [output.type, output.key, t, label, description, valueType, nodeId]);
return Render;
};

View File

@ -1,57 +0,0 @@
import React, { useMemo } from 'react';
import { Box, BoxProps } from '@chakra-ui/react';
import { Handle, OnConnect, Position } from 'reactflow';
import { FlowValueTypeMap } from '@/web/core/workflow/constants/dataType';
import MyTooltip from '@/components/MyTooltip';
import { useTranslation } from 'next-i18next';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
interface Props extends BoxProps {
handleKey: string;
valueType?: WorkflowIOValueTypeEnum;
}
const TargetHandle = ({ handleKey, valueType, ...props }: Props) => {
const { t } = useTranslation();
const valType = valueType ?? WorkflowIOValueTypeEnum.any;
const valueStyle = useMemo(
() =>
valueType && FlowValueTypeMap[valueType]
? FlowValueTypeMap[valueType]?.handlerStyle
: FlowValueTypeMap[WorkflowIOValueTypeEnum.any]?.handlerStyle,
[valueType]
);
return (
<Box
position={'absolute'}
top={'50%'}
left={'-18px'}
transform={'translate(0,-50%)'}
{...props}
>
<MyTooltip
label={t('app.module.type', {
type: t(FlowValueTypeMap[valType]?.label),
description: FlowValueTypeMap[valType]?.description
})}
>
<Handle
style={{
width: '14px',
height: '14px',
borderWidth: '3.5px',
backgroundColor: 'white',
...valueStyle
}}
type="target"
id={handleKey}
position={Position.Left}
/>
</MyTooltip>
</Box>
);
};
export default React.memo(TargetHandle);

View File

@ -1,21 +1,33 @@
import { FlowValueTypeMap } from '@/web/core/workflow/constants/dataType';
import { Box } from '@chakra-ui/react';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import React from 'react';
const ValueTypeLabel = ({ children }: { children: React.ReactNode }) => {
return (
<Box
bg={'myGray.100'}
color={'myGray.500'}
border={'base'}
borderRadius={'sm'}
ml={2}
px={1}
py={0.5}
fontSize={'11px'}
>
{children}
</Box>
);
const ValueTypeLabel = ({ valueType }: { valueType?: WorkflowIOValueTypeEnum }) => {
const valueTypeData = valueType ? FlowValueTypeMap[valueType] : undefined;
const label = valueTypeData?.label || '';
const description = valueTypeData?.description || '';
return !!label ? (
<MyTooltip label={description}>
<Box
bg={'myGray.100'}
color={'myGray.500'}
border={'base'}
borderRadius={'sm'}
ml={2}
px={1}
h={6}
display={'flex'}
alignItems={'center'}
fontSize={'11px'}
>
{label}
</Box>
</MyTooltip>
) : null;
};
export default React.memo(ValueTypeLabel);

View File

@ -1,17 +1,6 @@
import React from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import {
Box,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Button,
Flex
} from '@chakra-ui/react';
import { Box, Table, Thead, Tbody, Tr, Th, Td, TableContainer, Flex } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import type {
EditInputFieldMapType,

View File

@ -108,7 +108,7 @@ const LafAccountModal = ({
</Box>
<Box>
<Link textDecoration={'underline'} href={`${feConfigs.lafEnv}/`} isExternal>
{t('support.user.Go laf env')}
{t('support.user.Go laf env', { env: feConfigs.lafEnv })}
</Link>
</Box>
</Box>

View File

@ -1,6 +1,5 @@
import type { LLMModelItemType } from '../ai/model.d';
import { AppTypeEnum } from './constants';
import { AppSchema } from './type';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { AppSchema } from '@fastgpt/global/core/app/type';
export type CreateAppParams = {
name?: string;
@ -10,13 +9,19 @@ export type CreateAppParams = {
edges?: AppSchema['edges'];
};
export interface AppUpdateParams {
export type AppUpdateParams = {
name?: string;
type?: `${AppTypeEnum}`;
avatar?: string;
intro?: string;
modules?: AppSchema['modules'];
nodes?: AppSchema['modules'];
edges?: AppSchema['edges'];
permission?: AppSchema['permission'];
teamTags?: AppSchema['teamTags'];
}
};
export type PostPublishAppProps = {
type: `${AppTypeEnum}`;
nodes: AppSchema['modules'];
edges: AppSchema['edges'];
};

View File

@ -1,52 +1,68 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import type { CreateAppParams } from '@fastgpt/global/core/app/api.d';
import type { CreateAppParams } from '@/global/core/app/api.d';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { authUserNotVisitor } from '@fastgpt/service/support/permission/auth/user';
import { checkTeamAppLimit } from '@fastgpt/service/support/permission/teamLimit';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { MongoAppVersion } from '@fastgpt/service/core/app/versionSchema';
import { NextAPI } from '@/service/middle/entry';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const {
name = 'APP',
avatar,
type = AppTypeEnum.advanced,
modules,
edges
} = req.body as CreateAppParams;
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const {
name = 'APP',
avatar,
type = AppTypeEnum.advanced,
modules,
edges
} = req.body as CreateAppParams;
if (!name || !Array.isArray(modules)) {
throw new Error('缺少参数');
}
// 凭证校验
const { teamId, tmbId } = await authUserNotVisitor({ req, authToken: true });
// 上限校验
await checkTeamAppLimit(teamId);
// 创建模型
const response = await MongoApp.create({
avatar,
name,
teamId,
tmbId,
modules,
edges,
type,
version: 'v2'
});
jsonRes(res, {
data: response._id
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
if (!name || !Array.isArray(modules)) {
throw new Error('缺少参数');
}
// 凭证校验
const { teamId, tmbId } = await authUserNotVisitor({ req, authToken: true });
// 上限校验
await checkTeamAppLimit(teamId);
// 创建模型
const appId = await mongoSessionRun(async (session) => {
const [{ _id: appId }] = await MongoApp.create(
[
{
avatar,
name,
teamId,
tmbId,
modules,
edges,
type,
version: 'v2'
}
],
{ session }
);
await MongoAppVersion.create(
[
{
appId,
nodes: modules,
edges
}
],
{ session }
);
return appId;
});
jsonRes(res, {
data: appId
});
}
export default NextAPI(handler);

View File

@ -1,60 +1,59 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { MongoAppVersion } from '@fastgpt/service/core/app/versionSchema';
import { NextAPI } from '@/service/middle/entry';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { appId } = req.query as { appId: string };
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { appId } = req.query as { appId: string };
if (!appId) {
throw new Error('参数错误');
}
// 凭证校验
await authApp({ req, authToken: true, appId, per: 'owner' });
// 删除对应的聊天
await mongoSessionRun(async (session) => {
await MongoChatItem.deleteMany(
{
appId
},
{ session }
);
await MongoChat.deleteMany(
{
appId
},
{ session }
);
// 删除分享链接
await MongoOutLink.deleteMany(
{
appId
},
{ session }
);
// delete app
await MongoApp.deleteOne(
{
_id: appId
},
{ session }
);
});
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
if (!appId) {
throw new Error('参数错误');
}
// 凭证校验
await authApp({ req, authToken: true, appId, per: 'owner' });
// 删除对应的聊天
await mongoSessionRun(async (session) => {
await MongoChatItem.deleteMany(
{
appId
},
{ session }
);
await MongoChat.deleteMany(
{
appId
},
{ session }
);
// 删除分享链接
await MongoOutLink.deleteMany(
{
appId
},
{ session }
);
// delete version
await MongoAppVersion.deleteMany(
{
appId
},
{ session }
);
// delete app
await MongoApp.deleteOne(
{
_id: appId
},
{ session }
);
});
}
export default NextAPI(handler);

View File

@ -2,27 +2,20 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { NextAPI } from '@/service/middle/entry';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { appId } = req.query as { appId: string };
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { appId } = req.query as { appId: string };
if (!appId) {
throw new Error('参数错误');
}
// 凭证校验
const { app } = await authApp({ req, authToken: true, appId, per: 'w' });
jsonRes(res, {
data: app
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
if (!appId) {
throw new Error('参数错误');
}
// 凭证校验
const { app } = await authApp({ req, authToken: true, appId, per: 'w' });
return app;
}
export default NextAPI(handler);

View File

@ -1,6 +1,4 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import type { PagingData } from '@/types';
import { AppLogsListItemType } from '@/types/app';
@ -9,144 +7,140 @@ import { addDays } from 'date-fns';
import type { GetAppChatLogsParams } from '@/global/core/api/appReq.d';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { ChatItemCollectionName } from '@fastgpt/service/core/chat/chatItemSchema';
import { NextAPI } from '@/service/middle/entry';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const {
pageNum = 1,
pageSize = 20,
appId,
dateStart = addDays(new Date(), -7),
dateEnd = new Date()
} = req.body as GetAppChatLogsParams;
async function handler(
req: NextApiRequest,
res: NextApiResponse
): Promise<PagingData<AppLogsListItemType>> {
const {
pageNum = 1,
pageSize = 20,
appId,
dateStart = addDays(new Date(), -7),
dateEnd = new Date()
} = req.body as GetAppChatLogsParams;
if (!appId) {
throw new Error('缺少参数');
if (!appId) {
throw new Error('缺少参数');
}
// 凭证校验
const { teamId } = await authApp({ req, authToken: true, appId, per: 'w' });
const where = {
teamId: new Types.ObjectId(teamId),
appId: new Types.ObjectId(appId),
updateTime: {
$gte: new Date(dateStart),
$lte: new Date(dateEnd)
}
};
// 凭证校验
const { teamId } = await authApp({ req, authToken: true, appId, per: 'w' });
const where = {
teamId: new Types.ObjectId(teamId),
appId: new Types.ObjectId(appId),
updateTime: {
$gte: new Date(dateStart),
$lte: new Date(dateEnd)
}
};
const [data, total] = await Promise.all([
MongoChat.aggregate([
{ $match: where },
{
$sort: {
userBadFeedbackCount: -1,
userGoodFeedbackCount: -1,
customFeedbacksCount: -1,
updateTime: -1
}
},
{ $skip: (pageNum - 1) * pageSize },
{ $limit: pageSize },
{
$lookup: {
from: ChatItemCollectionName,
let: { chatId: '$chatId' },
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: ['$appId', new Types.ObjectId(appId)] },
{ $eq: ['$chatId', '$$chatId'] }
]
}
}
},
{
$project: {
userGoodFeedback: 1,
userBadFeedback: 1,
customFeedbacks: 1,
adminFeedback: 1
}
}
],
as: 'chatitems'
}
},
{
$addFields: {
userGoodFeedbackCount: {
$size: {
$filter: {
input: '$chatitems',
as: 'item',
cond: { $ifNull: ['$$item.userGoodFeedback', false] }
const [data, total] = await Promise.all([
MongoChat.aggregate([
{ $match: where },
{
$sort: {
userBadFeedbackCount: -1,
userGoodFeedbackCount: -1,
customFeedbacksCount: -1,
updateTime: -1
}
},
{ $skip: (pageNum - 1) * pageSize },
{ $limit: pageSize },
{
$lookup: {
from: ChatItemCollectionName,
let: { chatId: '$chatId' },
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: ['$appId', new Types.ObjectId(appId)] },
{ $eq: ['$chatId', '$$chatId'] }
]
}
}
},
userBadFeedbackCount: {
$size: {
$filter: {
input: '$chatitems',
as: 'item',
cond: { $ifNull: ['$$item.userBadFeedback', false] }
}
{
$project: {
userGoodFeedback: 1,
userBadFeedback: 1,
customFeedbacks: 1,
adminFeedback: 1
}
},
customFeedbacksCount: {
$size: {
$filter: {
input: '$chatitems',
as: 'item',
cond: { $gt: [{ $size: { $ifNull: ['$$item.customFeedbacks', []] } }, 0] }
}
}
],
as: 'chatitems'
}
},
{
$addFields: {
userGoodFeedbackCount: {
$size: {
$filter: {
input: '$chatitems',
as: 'item',
cond: { $ifNull: ['$$item.userGoodFeedback', false] }
}
},
markCount: {
$size: {
$filter: {
input: '$chatitems',
as: 'item',
cond: { $ifNull: ['$$item.adminFeedback', false] }
}
}
},
userBadFeedbackCount: {
$size: {
$filter: {
input: '$chatitems',
as: 'item',
cond: { $ifNull: ['$$item.userBadFeedback', false] }
}
}
},
customFeedbacksCount: {
$size: {
$filter: {
input: '$chatitems',
as: 'item',
cond: { $gt: [{ $size: { $ifNull: ['$$item.customFeedbacks', []] } }, 0] }
}
}
},
markCount: {
$size: {
$filter: {
input: '$chatitems',
as: 'item',
cond: { $ifNull: ['$$item.adminFeedback', false] }
}
}
}
},
{
$project: {
_id: 1,
id: '$chatId',
title: 1,
source: 1,
time: '$updateTime',
messageCount: { $size: '$chatitems' },
userGoodFeedbackCount: 1,
userBadFeedbackCount: 1,
customFeedbacksCount: 1,
markCount: 1
}
}
]),
MongoChat.countDocuments(where)
]);
jsonRes<PagingData<AppLogsListItemType>>(res, {
data: {
pageNum,
pageSize,
data,
total
},
{
$project: {
_id: 1,
id: '$chatId',
title: 1,
source: 1,
time: '$updateTime',
messageCount: { $size: '$chatitems' },
userGoodFeedbackCount: 1,
userBadFeedbackCount: 1,
customFeedbacksCount: 1,
markCount: 1
}
}
});
} catch (error) {
jsonRes(res, {
code: 500,
error
});
}
]),
MongoChat.countDocuments(where)
]);
return {
pageNum,
pageSize,
data,
total
};
}
export default NextAPI(handler);

View File

@ -1,38 +1,30 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { mongoRPermission } from '@fastgpt/global/support/permission/utils';
import { AppListItemType } from '@fastgpt/global/core/app/type';
import { authUserRole } from '@fastgpt/service/support/permission/auth/user';
import { NextAPI } from '@/service/middle/entry';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
// 凭证校验
const { teamId, tmbId, teamOwner, role } = await authUserRole({ req, authToken: true });
async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<AppListItemType[]> {
// 凭证校验
const { teamId, tmbId, teamOwner, role } = await authUserRole({ req, authToken: true });
// 根据 userId 获取模型信息
const myApps = await MongoApp.find(
{ ...mongoRPermission({ teamId, tmbId, role }) },
'_id avatar name intro tmbId permission'
).sort({
updateTime: -1
});
jsonRes<AppListItemType[]>(res, {
data: myApps.map((app) => ({
_id: app._id,
avatar: app.avatar,
name: app.name,
intro: app.intro,
isOwner: teamOwner || String(app.tmbId) === tmbId,
permission: app.permission
}))
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
// 根据 userId 获取模型信息
const myApps = await MongoApp.find(
{ ...mongoRPermission({ teamId, tmbId, role }) },
'_id avatar name intro tmbId permission'
).sort({
updateTime: -1
});
return myApps.map((app) => ({
_id: app._id,
avatar: app.avatar,
name: app.name,
intro: app.intro,
isOwner: teamOwner || String(app.tmbId) === tmbId,
permission: app.permission
}));
}
export default NextAPI(handler);

View File

@ -2,106 +2,50 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import type { AppUpdateParams } from '@fastgpt/global/core/app/api';
import type { AppUpdateParams } from '@/global/core/app/api';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { getLLMModel } from '@fastgpt/service/core/ai/model';
import { getGuideModule, splitGuideModule } from '@fastgpt/global/core/workflow/utils';
import { getNextTimeByCronStringAndTimezone } from '@fastgpt/global/common/string/time';
import { getScheduleTriggerApp } from '@/service/core/app/utils';
import { beforeUpdateAppFormat } from '@fastgpt/service/core/app/controller';
import { NextAPI } from '@/service/middle/entry';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const {
name,
avatar,
type,
intro,
modules: nodes,
edges,
permission,
teamTags
} = req.body as AppUpdateParams;
const { appId } = req.query as { appId: string };
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { name, avatar, type, intro, nodes, edges, permission, teamTags } =
req.body as AppUpdateParams;
const { appId } = req.query as { appId: string };
if (!appId) {
throw new Error('appId is empty');
}
// 凭证校验
await authApp({ req, authToken: true, appId, per: permission ? 'owner' : 'w' });
// format nodes data
// 1. dataset search limit, less than model quoteMaxToken
if (nodes) {
let maxTokens = 3000;
nodes.forEach((item) => {
if (
item.flowNodeType === FlowNodeTypeEnum.chatNode ||
item.flowNodeType === FlowNodeTypeEnum.tools
) {
const model =
item.inputs.find((item) => item.key === NodeInputKeyEnum.aiModel)?.value || '';
const chatModel = getLLMModel(model);
const quoteMaxToken = chatModel.quoteMaxToken || 3000;
maxTokens = Math.max(maxTokens, quoteMaxToken);
}
});
nodes.forEach((item) => {
if (item.flowNodeType === FlowNodeTypeEnum.datasetSearchNode) {
item.inputs.forEach((input) => {
if (input.key === NodeInputKeyEnum.datasetMaxTokens) {
const val = input.value as number;
if (val > maxTokens) {
input.value = maxTokens;
}
}
});
}
});
}
// 2. get schedule plan
const { scheduledTriggerConfig } = splitGuideModule(getGuideModule(nodes || []));
// 更新模型
await MongoApp.updateOne(
{
_id: appId
},
{
name,
type,
avatar,
intro,
permission,
version: 'v2',
teamTags: teamTags,
...(nodes && {
modules: nodes
}),
...(edges && {
edges
}),
scheduledTriggerConfig,
scheduledTriggerNextTime: scheduledTriggerConfig
? getNextTimeByCronStringAndTimezone(scheduledTriggerConfig)
: null
}
);
getScheduleTriggerApp();
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
if (!appId) {
throw new Error('appId is empty');
}
// 凭证校验
await authApp({ req, authToken: true, appId, per: permission ? 'owner' : 'w' });
// format nodes data
// 1. dataset search limit, less than model quoteMaxToken
const { nodes: formatNodes } = beforeUpdateAppFormat({ nodes });
// 更新模型
await MongoApp.updateOne(
{
_id: appId
},
{
name,
type,
avatar,
intro,
permission,
version: 'v2',
...(teamTags && teamTags),
...(formatNodes && {
modules: formatNodes
}),
...(edges && {
edges
})
}
);
}
export default NextAPI(handler);

View File

@ -1,81 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import type { AppUpdateParams } from '@fastgpt/global/core/app/api';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { getLLMModel } from '@fastgpt/service/core/ai/model';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { name, avatar, type, intro, modules, permission, teamTags } =
req.body as AppUpdateParams;
const { appId } = req.query as { appId: string };
if (!appId) {
throw new Error('appId is empty');
}
// 凭证校验
await authApp({ req, authToken: true, appId, per: permission ? 'owner' : 'w' });
// check modules
// 1. dataset search limit, less than model quoteMaxToken
if (modules) {
let maxTokens = 3000;
modules.forEach((item) => {
if (item.flowNodeType === FlowNodeTypeEnum.chatNode) {
const model =
item.inputs.find((item) => item.key === NodeInputKeyEnum.aiModel)?.value || '';
const chatModel = getLLMModel(model);
const quoteMaxToken = chatModel.quoteMaxToken || 3000;
maxTokens = Math.max(maxTokens, quoteMaxToken);
}
});
modules.forEach((item) => {
if (item.flowNodeType === FlowNodeTypeEnum.datasetSearchNode) {
item.inputs.forEach((input) => {
if (input.key === NodeInputKeyEnum.datasetMaxTokens) {
const val = input.value as number;
if (val > maxTokens) {
input.value = maxTokens;
}
}
});
}
});
}
// 更新模型
await MongoApp.findOneAndUpdate(
{
_id: appId
},
{
name,
type,
avatar,
intro,
permission,
teamTags: teamTags,
...(modules && {
modules
})
}
);
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@ -0,0 +1,54 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { NextAPI } from '@/service/middle/entry';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { MongoAppVersion } from '@fastgpt/service/core/app/versionSchema';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { beforeUpdateAppFormat } from '@fastgpt/service/core/app/controller';
import { getGuideModule, splitGuideModule } from '@fastgpt/global/core/workflow/utils';
import { getNextTimeByCronStringAndTimezone } from '@fastgpt/global/common/string/time';
import { PostPublishAppProps } from '@/global/core/app/api';
type Response = {};
async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<{}> {
const { appId } = req.query as { appId: string };
const { nodes = [], edges = [], type } = req.body as PostPublishAppProps;
await authApp({ appId, req, per: 'w', authToken: true });
const { nodes: formatNodes } = beforeUpdateAppFormat({ nodes });
const { scheduledTriggerConfig } = splitGuideModule(getGuideModule(formatNodes || []));
await mongoSessionRun(async (session) => {
// create version histories
await MongoAppVersion.create(
[
{
appId,
nodes: formatNodes,
edges
}
],
{ session }
);
// update app
await MongoApp.findByIdAndUpdate(appId, {
modules: formatNodes,
edges,
updateTime: new Date(),
version: 'v2',
type,
scheduledTriggerConfig,
scheduledTriggerNextTime: scheduledTriggerConfig
? getNextTimeByCronStringAndTimezone(scheduledTriggerConfig)
: null
});
});
return {};
}
export default NextAPI(handler);

View File

@ -9,6 +9,7 @@ import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { getChatItems } from '@fastgpt/service/core/chat/controller';
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { getAppLatestVersion } from '@fastgpt/service/core/app/controller';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
@ -40,14 +41,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
// get app and history
const { history } = await getChatItems({
appId,
chatId,
limit: 30,
field: `dataId obj value adminFeedback userBadFeedback userGoodFeedback ${
DispatchNodeResponseKeyEnum.nodeResponse
} ${loadCustomFeedbacks ? 'customFeedbacks' : ''}`
});
const [{ history }, { nodes }] = await Promise.all([
getChatItems({
appId,
chatId,
limit: 30,
field: `dataId obj value adminFeedback userBadFeedback userGoodFeedback ${
DispatchNodeResponseKeyEnum.nodeResponse
} ${loadCustomFeedbacks ? 'customFeedbacks' : ''}`
}),
getAppLatestVersion(app._id, app)
]);
jsonRes<InitChatResponse>(res, {
data: {
@ -58,8 +62,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
variables: chat?.variables || {},
history,
app: {
userGuideModule: getGuideModule(app.modules),
chatModels: getChatModelNameListByModules(app.modules),
userGuideModule: getGuideModule(nodes),
chatModels: getChatModelNameListByModules(nodes),
name: app.name,
avatar: app.avatar,
intro: app.intro

View File

@ -14,6 +14,7 @@ import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { getAppLatestVersion } from '@fastgpt/service/core/app/controller';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
@ -40,14 +41,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
throw new Error(ChatErrEnum.unAuthChat);
}
const { history } = await getChatItems({
appId: app._id,
chatId,
limit: 30,
field: `dataId obj value userGoodFeedback userBadFeedback ${
shareChat.responseDetail ? `adminFeedback ${DispatchNodeResponseKeyEnum.nodeResponse}` : ''
} `
});
const [{ history }, { nodes }] = await Promise.all([
getChatItems({
appId: app._id,
chatId,
limit: 30,
field: `dataId obj value userGoodFeedback userBadFeedback ${
shareChat.responseDetail
? `adminFeedback ${DispatchNodeResponseKeyEnum.nodeResponse}`
: ''
} `
}),
getAppLatestVersion(app._id, app)
]);
// pick share response field
history.forEach((item) => {
@ -66,8 +72,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
variables: chat?.variables || {},
history,
app: {
userGuideModule: getGuideModule(app.modules),
chatModels: getChatModelNameListByModules(app.modules),
userGuideModule: getGuideModule(nodes),
chatModels: getChatModelNameListByModules(nodes),
name: app.name,
avatar: app.avatar,
intro: app.intro

View File

@ -14,6 +14,7 @@ import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { filterPublicNodeResponseData } from '@fastgpt/global/core/chat/utils';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { getAppLatestVersion } from '@fastgpt/service/core/app/controller';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
@ -46,12 +47,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
// get app and history
const { history } = await getChatItems({
appId,
chatId,
limit: 30,
field: `dataId obj value userGoodFeedback userBadFeedback adminFeedback ${DispatchNodeResponseKeyEnum.nodeResponse}`
});
const [{ history }, { nodes }] = await Promise.all([
getChatItems({
appId,
chatId,
limit: 30,
field: `dataId obj value userGoodFeedback userBadFeedback adminFeedback ${DispatchNodeResponseKeyEnum.nodeResponse}`
}),
getAppLatestVersion(app._id, app)
]);
// pick share response field
history.forEach((item) => {
@ -69,8 +73,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
variables: chat?.variables || {},
history,
app: {
userGuideModule: getGuideModule(app.modules),
chatModels: getChatModelNameListByModules(app.modules),
userGuideModule: getGuideModule(nodes),
chatModels: getChatModelNameListByModules(nodes),
name: app.name,
avatar: app.avatar,
intro: app.intro

View File

@ -1,40 +1,32 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { withNextCors } from '@fastgpt/service/common/middle/cors';
import { connectToDatabase } from '@/service/mongo';
import { authDatasetData } from '@/service/support/permission/auth/dataset';
import { deleteDatasetData } from '@/service/core/dataset/data/controller';
import { NextAPI } from '@/service/middle/entry';
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { id: dataId } = req.query as {
id: string;
};
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { id: dataId } = req.query as {
id: string;
};
if (!dataId) {
throw new Error('dataId is required');
}
// 凭证校验
const { teamId, datasetData } = await authDatasetData({
req,
authToken: true,
authApiKey: true,
dataId,
per: 'w'
});
await deleteDatasetData(datasetData);
jsonRes(res, {
data: 'success'
});
} catch (err) {
console.log(err);
jsonRes(res, {
code: 500,
error: err
});
if (!dataId) {
throw new Error('dataId is required');
}
});
// 凭证校验
const { teamId, datasetData } = await authDatasetData({
req,
authToken: true,
authApiKey: true,
dataId,
per: 'w'
});
await deleteDatasetData(datasetData);
jsonRes(res, {
data: 'success'
});
}
export default NextAPI(handler);

View File

@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { authDatasetData } from '@/service/support/permission/auth/dataset';
import { NextAPI } from '@/service/middle/entry';
export type Response = {
id: string;
@ -10,29 +11,23 @@ export type Response = {
source: string;
};
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { id: dataId } = req.query as {
id: string;
};
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { id: dataId } = req.query as {
id: string;
};
// 凭证校验
const { datasetData } = await authDatasetData({
req,
authToken: true,
authApiKey: true,
dataId,
per: 'r'
});
// 凭证校验
const { datasetData } = await authDatasetData({
req,
authToken: true,
authApiKey: true,
dataId,
per: 'r'
});
jsonRes(res, {
data: datasetData
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
jsonRes(res, {
data: datasetData
});
}
export default NextAPI(handler);

View File

@ -5,7 +5,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { withNextCors } from '@fastgpt/service/common/middle/cors';
import { countPromptTokens } from '@fastgpt/service/common/string/tiktoken/index';
import { getVectorModel } from '@fastgpt/service/core/ai/model';
import { hasSameValue } from '@/service/core/dataset/data/utils';
@ -16,92 +15,87 @@ import { pushGenerateVectorUsage } from '@/service/support/wallet/usage/push';
import { InsertOneDatasetDataProps } from '@/global/core/dataset/api';
import { simpleText } from '@fastgpt/global/common/string/tools';
import { checkDatasetLimit } from '@fastgpt/service/support/permission/teamLimit';
import { NextAPI } from '@/service/middle/entry';
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { collectionId, q, a, indexes } = req.body as InsertOneDatasetDataProps;
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { collectionId, q, a, indexes } = req.body as InsertOneDatasetDataProps;
if (!q) {
throw new Error('q is required');
}
if (!collectionId) {
throw new Error('collectionId is required');
}
// 凭证校验
const { teamId, tmbId } = await authDatasetCollection({
req,
authToken: true,
authApiKey: true,
collectionId,
per: 'w'
});
await checkDatasetLimit({
teamId,
insertLen: 1
});
// auth collection and get dataset
const [
{
datasetId: { _id: datasetId, vectorModel }
}
] = await Promise.all([getCollectionWithDataset(collectionId)]);
// format data
const formatQ = simpleText(q);
const formatA = simpleText(a);
const formatIndexes = indexes?.map((item) => ({
...item,
text: simpleText(item.text)
}));
// token check
const token = await countPromptTokens(formatQ + formatA, '');
const vectorModelData = getVectorModel(vectorModel);
if (token > vectorModelData.maxToken) {
return Promise.reject('Q Over Tokens');
}
// Duplicate data check
await hasSameValue({
teamId,
datasetId,
collectionId,
q: formatQ,
a: formatA
});
const { insertId, tokens } = await insertData2Dataset({
teamId,
tmbId,
datasetId,
collectionId,
q: formatQ,
a: formatA,
chunkIndex: 0,
model: vectorModelData.model,
indexes: formatIndexes
});
pushGenerateVectorUsage({
teamId,
tmbId,
tokens,
model: vectorModelData.model
});
jsonRes<string>(res, {
data: insertId
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
if (!q) {
throw new Error('q is required');
}
});
if (!collectionId) {
throw new Error('collectionId is required');
}
// 凭证校验
const { teamId, tmbId } = await authDatasetCollection({
req,
authToken: true,
authApiKey: true,
collectionId,
per: 'w'
});
await checkDatasetLimit({
teamId,
insertLen: 1
});
// auth collection and get dataset
const [
{
datasetId: { _id: datasetId, vectorModel }
}
] = await Promise.all([getCollectionWithDataset(collectionId)]);
// format data
const formatQ = simpleText(q);
const formatA = simpleText(a);
const formatIndexes = indexes?.map((item) => ({
...item,
text: simpleText(item.text)
}));
// token check
const token = await countPromptTokens(formatQ + formatA, '');
const vectorModelData = getVectorModel(vectorModel);
if (token > vectorModelData.maxToken) {
return Promise.reject('Q Over Tokens');
}
// Duplicate data check
await hasSameValue({
teamId,
datasetId,
collectionId,
q: formatQ,
a: formatA
});
const { insertId, tokens } = await insertData2Dataset({
teamId,
tmbId,
datasetId,
collectionId,
q: formatQ,
a: formatA,
chunkIndex: 0,
model: vectorModelData.model,
indexes: formatIndexes
});
pushGenerateVectorUsage({
teamId,
tmbId,
tokens,
model: vectorModelData.model
});
jsonRes<string>(res, {
data: insertId
});
}
export default NextAPI(handler);

View File

@ -7,62 +7,57 @@ import { authDatasetCollection } from '@fastgpt/service/support/permission/auth/
import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema';
import { PagingData } from '@/types';
import { replaceRegChars } from '@fastgpt/global/common/string/tools';
import { NextAPI } from '@/service/middle/entry';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
let {
pageNum = 1,
pageSize = 10,
searchText = '',
collectionId
} = req.body as GetDatasetDataListProps;
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
let {
pageNum = 1,
pageSize = 10,
searchText = '',
collectionId
} = req.body as GetDatasetDataListProps;
pageSize = Math.min(pageSize, 30);
pageSize = Math.min(pageSize, 30);
// 凭证校验
const { teamId, collection } = await authDatasetCollection({
req,
authToken: true,
authApiKey: true,
collectionId,
per: 'r'
});
// 凭证校验
const { teamId, collection } = await authDatasetCollection({
req,
authToken: true,
authApiKey: true,
collectionId,
per: 'r'
});
searchText = replaceRegChars(searchText).replace(/'/g, '');
searchText = replaceRegChars(searchText).replace(/'/g, '');
const match = {
teamId,
datasetId: collection.datasetId._id,
collectionId,
...(searchText
? {
$or: [{ q: new RegExp(searchText, 'i') }, { a: new RegExp(searchText, 'i') }]
}
: {})
};
const match = {
teamId,
datasetId: collection.datasetId._id,
collectionId,
...(searchText
? {
$or: [{ q: new RegExp(searchText, 'i') }, { a: new RegExp(searchText, 'i') }]
}
: {})
};
const [data, total] = await Promise.all([
MongoDatasetData.find(match, '_id datasetId collectionId q a chunkIndex')
.sort({ chunkIndex: 1, updateTime: -1 })
.skip((pageNum - 1) * pageSize)
.limit(pageSize)
.lean(),
MongoDatasetData.countDocuments(match)
]);
const [data, total] = await Promise.all([
MongoDatasetData.find(match, '_id datasetId collectionId q a chunkIndex')
.sort({ chunkIndex: 1, updateTime: -1 })
.skip((pageNum - 1) * pageSize)
.limit(pageSize)
.lean(),
MongoDatasetData.countDocuments(match)
]);
jsonRes<PagingData<DatasetDataListItemType>>(res, {
data: {
pageNum,
pageSize,
data,
total
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
jsonRes<PagingData<DatasetDataListItemType>>(res, {
data: {
pageNum,
pageSize,
data,
total
}
});
}
export default NextAPI(handler);

View File

@ -2,7 +2,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { withNextCors } from '@fastgpt/service/common/middle/cors';
import type {
PushDatasetDataProps,
PushDatasetDataResponse
@ -11,53 +10,48 @@ import { authDatasetCollection } from '@fastgpt/service/support/permission/auth/
import { checkDatasetLimit } from '@fastgpt/service/support/permission/teamLimit';
import { predictDataLimitLength } from '@fastgpt/global/core/dataset/utils';
import { pushDataListToTrainingQueue } from '@fastgpt/service/core/dataset/training/controller';
import { NextAPI } from '@/service/middle/entry';
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const body = req.body as PushDatasetDataProps;
const { collectionId, data } = body;
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const body = req.body as PushDatasetDataProps;
const { collectionId, data } = body;
if (!collectionId || !Array.isArray(data)) {
throw new Error('collectionId or data is empty');
}
if (data.length > 200) {
throw new Error('Data is too long, max 200');
}
// 凭证校验
const { teamId, tmbId, collection } = await authDatasetCollection({
req,
authToken: true,
authApiKey: true,
collectionId,
per: 'w'
});
// auth dataset limit
await checkDatasetLimit({
teamId,
insertLen: predictDataLimitLength(collection.trainingType, data)
});
jsonRes<PushDatasetDataResponse>(res, {
data: await pushDataListToTrainingQueue({
...body,
teamId,
tmbId,
datasetId: collection.datasetId._id,
agentModel: collection.datasetId.agentModel,
vectorModel: collection.datasetId.vectorModel
})
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
if (!collectionId || !Array.isArray(data)) {
throw new Error('collectionId or data is empty');
}
});
if (data.length > 200) {
throw new Error('Data is too long, max 200');
}
// 凭证校验
const { teamId, tmbId, collection } = await authDatasetCollection({
req,
authToken: true,
authApiKey: true,
collectionId,
per: 'w'
});
// auth dataset limit
await checkDatasetLimit({
teamId,
insertLen: predictDataLimitLength(collection.trainingType, data)
});
jsonRes<PushDatasetDataResponse>(res, {
data: await pushDataListToTrainingQueue({
...body,
teamId,
tmbId,
datasetId: collection.datasetId._id,
agentModel: collection.datasetId.agentModel,
vectorModel: collection.datasetId.vectorModel
})
});
}
export default NextAPI(handler);
export const config = {
api: {

View File

@ -1,59 +1,53 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { withNextCors } from '@fastgpt/service/common/middle/cors';
import { connectToDatabase } from '@/service/mongo';
import { updateData2Dataset } from '@/service/core/dataset/data/controller';
import { authDatasetData } from '@/service/support/permission/auth/dataset';
import { pushGenerateVectorUsage } from '@/service/support/wallet/usage/push';
import { UpdateDatasetDataProps } from '@/global/core/dataset/api';
import { checkDatasetLimit } from '@fastgpt/service/support/permission/teamLimit';
import { NextAPI } from '@/service/middle/entry';
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { id, q = '', a, indexes = [] } = req.body as UpdateDatasetDataProps;
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { id, q = '', a, indexes = [] } = req.body as UpdateDatasetDataProps;
// auth data permission
const {
collection: {
datasetId: { vectorModel }
},
teamId,
tmbId
} = await authDatasetData({
req,
authToken: true,
authApiKey: true,
dataId: id,
per: 'w'
});
// auth data permission
const {
collection: {
datasetId: { vectorModel }
},
teamId,
tmbId
} = await authDatasetData({
req,
authToken: true,
authApiKey: true,
dataId: id,
per: 'w'
});
// auth team balance
await checkDatasetLimit({
teamId,
insertLen: 1
});
// auth team balance
await checkDatasetLimit({
teamId,
insertLen: 1
});
const { tokens } = await updateData2Dataset({
dataId: id,
q,
a,
indexes,
model: vectorModel
});
const { tokens } = await updateData2Dataset({
dataId: id,
q,
a,
indexes,
model: vectorModel
});
pushGenerateVectorUsage({
teamId,
tmbId,
tokens,
model: vectorModel
});
pushGenerateVectorUsage({
teamId,
tmbId,
tokens,
model: vectorModel
});
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
});
jsonRes(res);
}
export default NextAPI(handler);

View File

@ -1,94 +1,85 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes, responseWriteController } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { responseWriteController } from '@fastgpt/service/common/response';
import { addLog } from '@fastgpt/service/common/system/log';
import { authDataset } from '@fastgpt/service/support/permission/auth/dataset';
import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema';
import { findDatasetAndAllChildren } from '@fastgpt/service/core/dataset/controller';
import { withNextCors } from '@fastgpt/service/common/middle/cors';
import {
checkExportDatasetLimit,
updateExportDatasetLimit
} from '@fastgpt/service/support/user/utils';
import { NextAPI } from '@/service/middle/entry';
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
let { datasetId } = req.query as {
datasetId: string;
};
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
let { datasetId } = req.query as {
datasetId: string;
};
if (!datasetId || !global.pgClient) {
throw new Error('缺少参数');
}
// 凭证校验
const { teamId } = await authDataset({ req, authToken: true, datasetId, per: 'w' });
await checkExportDatasetLimit({
teamId,
limitMinutes: global.feConfigs?.limit?.exportDatasetLimitMinutes
});
const datasets = await findDatasetAndAllChildren({
teamId,
datasetId,
fields: '_id'
});
res.setHeader('Content-Type', 'text/csv; charset=utf-8;');
res.setHeader('Content-Disposition', 'attachment; filename=dataset.csv; ');
const cursor = MongoDatasetData.find<{
_id: string;
collectionId: { name: string };
q: string;
a: string;
}>(
{
teamId,
datasetId: { $in: datasets.map((d) => d._id) }
},
'q a'
)
.limit(50000)
.cursor();
const write = responseWriteController({
res,
readStream: cursor
});
write(`\uFEFFindex,content`);
cursor.on('data', (doc) => {
const q = doc.q.replace(/"/g, '""') || '';
const a = doc.a.replace(/"/g, '""') || '';
write(`\n"${q}","${a}"`);
});
cursor.on('end', () => {
cursor.close();
res.end();
});
cursor.on('error', (err) => {
addLog.error(`export dataset error`, err);
res.status(500);
res.end();
});
updateExportDatasetLimit(teamId);
} catch (err) {
res.status(500);
addLog.error(`export dataset error`, err);
jsonRes(res, {
code: 500,
error: err
});
if (!datasetId || !global.pgClient) {
throw new Error('缺少参数');
}
});
// 凭证校验
const { teamId } = await authDataset({ req, authToken: true, datasetId, per: 'w' });
await checkExportDatasetLimit({
teamId,
limitMinutes: global.feConfigs?.limit?.exportDatasetLimitMinutes
});
const datasets = await findDatasetAndAllChildren({
teamId,
datasetId,
fields: '_id'
});
res.setHeader('Content-Type', 'text/csv; charset=utf-8;');
res.setHeader('Content-Disposition', 'attachment; filename=dataset.csv; ');
const cursor = MongoDatasetData.find<{
_id: string;
collectionId: { name: string };
q: string;
a: string;
}>(
{
teamId,
datasetId: { $in: datasets.map((d) => d._id) }
},
'q a'
)
.limit(50000)
.cursor();
const write = responseWriteController({
res,
readStream: cursor
});
write(`\uFEFFindex,content`);
cursor.on('data', (doc) => {
const q = doc.q.replace(/"/g, '""') || '';
const a = doc.a.replace(/"/g, '""') || '';
write(`\n"${q}","${a}"`);
});
cursor.on('end', () => {
cursor.close();
res.end();
});
cursor.on('error', (err) => {
addLog.error(`export dataset error`, err);
res.status(500);
res.end();
});
updateExportDatasetLimit(teamId);
}
export default NextAPI(handler);
export const config = {
api: {

View File

@ -1,54 +1,48 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import type { DatasetListItemType } from '@fastgpt/global/core/dataset/type.d';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { MongoDataset } from '@fastgpt/service/core/dataset/schema';
import { mongoRPermission } from '@fastgpt/global/support/permission/utils';
import { authUserRole } from '@fastgpt/service/support/permission/auth/user';
import { getVectorModel } from '@fastgpt/service/core/ai/model';
import { NextAPI } from '@/service/middle/entry';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { parentId, type } = req.query as { parentId?: string; type?: `${DatasetTypeEnum}` };
// 凭证校验
const { teamId, tmbId, teamOwner, role, canWrite } = await authUserRole({
req,
authToken: true,
authApiKey: true
});
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { parentId, type } = req.query as { parentId?: string; type?: `${DatasetTypeEnum}` };
// 凭证校验
const { teamId, tmbId, teamOwner, role, canWrite } = await authUserRole({
req,
authToken: true,
authApiKey: true
});
const datasets = await MongoDataset.find({
...mongoRPermission({ teamId, tmbId, role }),
...(parentId !== undefined && { parentId: parentId || null }),
...(type && { type })
})
.sort({ updateTime: -1 })
.lean();
const datasets = await MongoDataset.find({
...mongoRPermission({ teamId, tmbId, role }),
...(parentId !== undefined && { parentId: parentId || null }),
...(type && { type })
})
.sort({ updateTime: -1 })
.lean();
const data = await Promise.all(
datasets.map<DatasetListItemType>((item) => ({
_id: item._id,
parentId: item.parentId,
avatar: item.avatar,
name: item.name,
intro: item.intro,
type: item.type,
permission: item.permission,
canWrite,
isOwner: teamOwner || String(item.tmbId) === tmbId,
vectorModel: getVectorModel(item.vectorModel)
}))
);
const data = await Promise.all(
datasets.map<DatasetListItemType>((item) => ({
_id: item._id,
parentId: item.parentId,
avatar: item.avatar,
name: item.name,
intro: item.intro,
type: item.type,
permission: item.permission,
canWrite,
isOwner: teamOwner || String(item.tmbId) === tmbId,
vectorModel: getVectorModel(item.vectorModel)
}))
);
jsonRes<DatasetListItemType[]>(res, {
data
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
jsonRes<DatasetListItemType[]>(res, {
data
});
}
export default NextAPI(handler);

View File

@ -1,8 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { withNextCors } from '@fastgpt/service/common/middle/cors';
import type { SearchTestProps, SearchTestResponse } from '@/global/core/dataset/api.d';
import { connectToDatabase } from '@/service/mongo';
import { authDataset } from '@fastgpt/service/support/permission/auth/dataset';
import { pushGenerateVectorUsage } from '@/service/support/wallet/usage/push';
import { searchDatasetData } from '@fastgpt/service/core/dataset/search/controller';
@ -14,95 +12,90 @@ import {
checkTeamAIPoints,
checkTeamReRankPermission
} from '@fastgpt/service/support/permission/teamLimit';
import { NextAPI } from '@/service/middle/entry';
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const {
datasetId,
text,
limit = 1500,
similarity,
searchMode,
usingReRank,
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const {
datasetId,
text,
limit = 1500,
similarity,
searchMode,
usingReRank,
datasetSearchUsingExtensionQuery = false,
datasetSearchExtensionModel,
datasetSearchExtensionBg = ''
} = req.body as SearchTestProps;
datasetSearchUsingExtensionQuery = false,
datasetSearchExtensionModel,
datasetSearchExtensionBg = ''
} = req.body as SearchTestProps;
if (!datasetId || !text) {
throw new Error('缺少参数');
}
const start = Date.now();
if (!datasetId || !text) {
throw new Error('缺少参数');
}
const start = Date.now();
// auth dataset role
const { dataset, teamId, tmbId, apikey } = await authDataset({
req,
authToken: true,
authApiKey: true,
datasetId,
per: 'r'
});
// auth balance
await checkTeamAIPoints(teamId);
// auth dataset role
const { dataset, teamId, tmbId, apikey } = await authDataset({
req,
authToken: true,
authApiKey: true,
datasetId,
per: 'r'
});
// auth balance
await checkTeamAIPoints(teamId);
// query extension
const extensionModel =
datasetSearchUsingExtensionQuery && datasetSearchExtensionModel
? getLLMModel(datasetSearchExtensionModel)
: undefined;
const { concatQueries, rewriteQuery, aiExtensionResult } = await datasetSearchQueryExtension({
query: text,
extensionModel,
extensionBg: datasetSearchExtensionBg
});
// query extension
const extensionModel =
datasetSearchUsingExtensionQuery && datasetSearchExtensionModel
? getLLMModel(datasetSearchExtensionModel)
: undefined;
const { concatQueries, rewriteQuery, aiExtensionResult } = await datasetSearchQueryExtension({
query: text,
extensionModel,
extensionBg: datasetSearchExtensionBg
});
const { searchRes, tokens, ...result } = await searchDatasetData({
teamId,
reRankQuery: rewriteQuery,
queries: concatQueries,
model: dataset.vectorModel,
limit: Math.min(limit, 20000),
similarity,
datasetIds: [datasetId],
searchMode,
usingReRank: usingReRank && (await checkTeamReRankPermission(teamId))
});
const { searchRes, tokens, ...result } = await searchDatasetData({
teamId,
reRankQuery: rewriteQuery,
queries: concatQueries,
model: dataset.vectorModel,
limit: Math.min(limit, 20000),
similarity,
datasetIds: [datasetId],
searchMode,
usingReRank: usingReRank && (await checkTeamReRankPermission(teamId))
});
// push bill
const { totalPoints } = pushGenerateVectorUsage({
teamId,
tmbId,
tokens,
model: dataset.vectorModel,
source: apikey ? UsageSourceEnum.api : UsageSourceEnum.fastgpt,
// push bill
const { totalPoints } = pushGenerateVectorUsage({
teamId,
tmbId,
tokens,
model: dataset.vectorModel,
source: apikey ? UsageSourceEnum.api : UsageSourceEnum.fastgpt,
...(aiExtensionResult &&
extensionModel && {
extensionModel: extensionModel.name,
extensionTokens: aiExtensionResult.tokens
})
});
if (apikey) {
updateApiKeyUsage({
apikey,
totalPoints: totalPoints
});
}
jsonRes<SearchTestResponse>(res, {
data: {
list: searchRes,
duration: `${((Date.now() - start) / 1000).toFixed(3)}s`,
usingQueryExtension: !!aiExtensionResult,
...result
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
...(aiExtensionResult &&
extensionModel && {
extensionModel: extensionModel.name,
extensionTokens: aiExtensionResult.tokens
})
});
if (apikey) {
updateApiKeyUsage({
apikey,
totalPoints: totalPoints
});
}
});
jsonRes<SearchTestResponse>(res, {
data: {
list: searchRes,
duration: `${((Date.now() - start) / 1000).toFixed(3)}s`,
usingQueryExtension: !!aiExtensionResult,
...result
}
});
}
export default NextAPI(handler);

View File

@ -1,6 +1,5 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { withNextCors } from '@fastgpt/service/common/middle/cors';
import { getUploadModel } from '@fastgpt/service/common/file/multer';
import { removeFilesByPaths } from '@fastgpt/service/common/file/utils';
import fs from 'fs';
@ -10,12 +9,13 @@ import { authChatCert } from '@/service/support/permission/auth/chat';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { getGuideModule, splitGuideModule } from '@fastgpt/global/core/workflow/utils';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { NextAPI } from '@/service/middle/entry';
const upload = getUploadModel({
maxSize: 2
});
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
let filePaths: string[] = [];
try {
@ -81,7 +81,9 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
}
removeFilesByPaths(filePaths);
});
}
export default NextAPI(handler);
export const config = {
api: {

View File

@ -3,7 +3,6 @@ import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { sseErrRes, jsonRes } from '@fastgpt/service/common/response';
import { addLog } from '@fastgpt/service/common/system/log';
import { withNextCors } from '@fastgpt/service/common/middle/cors';
import { ChatRoleEnum, ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
@ -42,6 +41,9 @@ import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runti
import { dispatchWorkFlowV1 } from '@fastgpt/service/core/workflow/dispatchV1';
import { setEntryEntries } from '@fastgpt/service/core/workflow/dispatchV1/utils';
import { NextAPI } from '@/service/middle/entry';
import { MongoAppVersion } from '@fastgpt/service/core/app/versionSchema';
import { getAppLatestVersion } from '@fastgpt/service/core/app/controller';
type FastGptWebChatProps = {
chatId?: string; // undefined: nonuse history, '': new chat, 'xxxxx': use history
@ -73,7 +75,7 @@ type AuthResponseType = {
outLinkUserId?: string;
};
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse) {
async function handler(req: NextApiRequest, res: NextApiResponse) {
res.on('close', () => {
res.end();
});
@ -163,13 +165,16 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
});
})();
// get and concat history
const { history } = await getChatItems({
appId: app._id,
chatId,
limit: 30,
field: `dataId obj value`
});
// 1. get and concat history; 2. get app workflow
const [{ history }, { nodes, edges }] = await Promise.all([
getChatItems({
appId: app._id,
chatId,
limit: 30,
field: `dataId obj value`
}),
getAppLatestVersion(app._id, app)
]);
const concatHistories = history.concat(chatMessages);
const responseChatItemId: string | undefined = messages[messages.length - 1].dataId;
@ -185,8 +190,8 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
appId: String(app._id),
chatId,
responseChatItemId,
runtimeNodes: storeNodes2RuntimeNodes(app.modules, getDefaultEntryNodeIds(app.modules)),
runtimeEdges: initWorkflowEdgeStatus(app.edges),
runtimeNodes: storeNodes2RuntimeNodes(nodes, getDefaultEntryNodeIds(nodes)),
runtimeEdges: initWorkflowEdgeStatus(edges),
variables: {
...variables,
userChatInput: text
@ -349,7 +354,8 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
});
}
}
});
}
export default NextAPI(handler);
export const config = {
api: {

View File

@ -1,7 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { withNextCors } from '@fastgpt/service/common/middle/cors';
import { pushGenerateVectorUsage } from '@/service/support/wallet/usage/push';
import { connectToDatabase } from '@/service/mongo';
import { getVectorsByText } from '@fastgpt/service/core/ai/embedding';
@ -19,7 +18,7 @@ type Props = {
type: `${EmbeddingTypeEnm}`;
};
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
let { input, model, billId, type } = req.body as Props;
await connectToDatabase();
@ -80,4 +79,4 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
error: err
});
}
});
}

View File

@ -24,6 +24,10 @@ import {
checkWorkflowNodeAndConnection,
filterSensitiveNodesData
} from '@/web/core/workflow/utils';
import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useQuery } from '@tanstack/react-query';
import { formatTime2HM } from '@fastgpt/global/common/string/time';
const ImportSettings = dynamic(() => import('@/components/core/workflow/Flow/ImportSettings'));
@ -50,13 +54,14 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
const { toast } = useToast();
const { t } = useTranslation();
const { copyData } = useCopyData();
const { openConfirm: openConfirmOut, ConfirmModal } = useConfirm({
content: t('core.app.edit.Out Ad Edit')
const { openConfirm: openConfigPublish, ConfirmModal } = useConfirm({
content: t('core.app.Publish Confirm')
});
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
const { updateAppDetail } = useAppStore();
const { publishApp, updateAppDetail } = useAppStore();
const { edges, onUpdateNodeError } = useFlowProviderStore();
const [isSaving, setIsSaving] = useState(false);
const [saveLabel, setSaveLabel] = useState(t('core.app.Onclick to save'));
const flowData2StoreDataAndCheck = useCallback(async () => {
const { nodes } = await getWorkflowStore();
@ -75,48 +80,95 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
}
}, [edges, onUpdateNodeError, t, toast]);
const onclickSave = useCallback(
async ({ nodes, edges }: { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] }) => {
setIsSaving(true);
const onclickSave = useCallback(async () => {
const { nodes } = await getWorkflowStore();
if (nodes.length === 0) return null;
setIsSaving(true);
const storeWorkflow = flowNode2StoreNodes({ nodes, edges });
try {
await updateAppDetail(app._id, {
...storeWorkflow,
type: AppTypeEnum.advanced,
//@ts-ignore
version: 'v2'
});
setSaveLabel(
t('core.app.Auto Save time', {
time: formatTime2HM()
})
);
ChatTestRef.current?.resetChatTest();
} catch (error) {}
setIsSaving(false);
return null;
}, [updateAppDetail, app._id, edges, ChatTestRef, t]);
const onclickPublish = useCallback(async () => {
setIsSaving(true);
const data = await flowData2StoreDataAndCheck();
if (data) {
try {
await updateAppDetail(app._id, {
modules: nodes,
edges,
await publishApp(app._id, {
...data,
type: AppTypeEnum.advanced,
permission: undefined,
//@ts-ignore
version: 'v2'
});
toast({
status: 'success',
title: t('common.Save Success')
title: t('core.app.Publish Success')
});
ChatTestRef.current?.resetChatTest();
} catch (error) {
toast({
status: 'warning',
title: getErrText(error, t('common.Save Failed'))
title: getErrText(error, t('core.app.Publish Failed'))
});
}
setIsSaving(false);
},
[ChatTestRef, app._id, t, toast, updateAppDetail]
);
}
setIsSaving(false);
}, [flowData2StoreDataAndCheck, publishApp, app._id, toast, t, ChatTestRef]);
const saveAndBack = useCallback(async () => {
try {
const data = await flowData2StoreDataAndCheck();
if (data) {
await onclickSave(data);
}
await onclickSave();
onClose();
} catch (error) {
toast({
status: 'warning',
title: getErrText(error)
});
} catch (error) {}
}, [onClose, onclickSave]);
const onExportWorkflow = useCallback(async () => {
const data = await flowData2StoreDataAndCheck();
if (data) {
copyData(
JSON.stringify(
{
nodes: filterSensitiveNodesData(data.nodes),
edges: data.edges
},
null,
2
),
t('app.Export Config Successful')
);
}
}, [flowData2StoreDataAndCheck, onClose, onclickSave, toast]);
}, [copyData, flowData2StoreDataAndCheck, t]);
useBeforeunload({
callback: onclickSave,
tip: t('core.common.tip.leave page')
});
useQuery(['autoSave'], onclickSave, {
refetchInterval: 20 * 1000,
enabled: !!app._id
});
const Render = useMemo(() => {
return (
@ -139,12 +191,29 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
variant={'whiteBase'}
aria-label={''}
isLoading={isSaving}
onClick={openConfirmOut(saveAndBack, onClose)}
onClick={saveAndBack}
/>
<Box ml={[3, 6]} fontSize={['md', '2xl']} flex={1}>
{app.name}
<Box ml={[3, 5]}>
<Box fontSize={['md', 'lg']} fontWeight={'bold'}>
{app.name}
</Box>
<MyTooltip label={t('core.app.Onclick to save')}>
<Box
fontSize={'sm'}
mt={1}
display={'inline-block'}
borderRadius={'xs'}
cursor={'pointer'}
onClick={onclickSave}
color={'myGray.500'}
>
{saveLabel}
</Box>
</MyTooltip>
</Box>
<Box flex={1} />
<MyMenu
Button={
<IconButton
@ -164,22 +233,7 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
{
label: t('app.Export Configs'),
icon: 'export',
onClick: async () => {
const data = await flowData2StoreDataAndCheck();
if (data) {
copyData(
JSON.stringify(
{
nodes: filterSensitiveNodesData(data.nodes),
edges: data.edges
},
null,
2
),
t('app.Export Config Successful')
);
}
}
onClick: onExportWorkflow
}
]}
/>
@ -202,34 +256,27 @@ const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
<Button
size={'sm'}
isLoading={isSaving}
leftIcon={<MyIcon name={'common/saveFill'} w={['14px', '16px']} />}
onClick={async () => {
const modules = await flowData2StoreDataAndCheck();
if (modules) {
onclickSave(modules);
}
}}
leftIcon={<MyIcon name={'common/publishFill'} w={['14px', '16px']} />}
onClick={openConfigPublish(onclickPublish)}
>
{t('common.Save')}
{t('core.app.Publish')}
</Button>
</Flex>
<ConfirmModal
closeText={t('core.app.edit.UnSave')}
confirmText={t('core.app.edit.Save and out')}
/>
<ConfirmModal confirmText={t('core.app.Publish')} />
</>
);
}, [
ConfirmModal,
app.name,
copyData,
flowData2StoreDataAndCheck,
isSaving,
onClose,
onExportWorkflow,
onOpenImport,
onclickPublish,
onclickSave,
openConfirmOut,
openConfigPublish,
saveAndBack,
saveLabel,
setWorkflowTestData,
t,
theme.borders.base

View File

@ -19,17 +19,15 @@ const Render = ({ app, onClose }: Props) => {
const { initData } = useFlowProviderStore();
const workflowStringData = JSON.stringify({
nodes: app.modules || [],
edges: app.edges || []
});
useEffect(() => {
if (!isV2Workflow) return;
initData(
JSON.parse(
JSON.stringify({
nodes: app.modules || [],
edges: app.edges || []
})
)
);
}, [isV2Workflow, app.edges, app.modules]);
initData(JSON.parse(workflowStringData));
}, [isV2Workflow, initData, workflowStringData]);
useEffect(() => {
if (!isV2Workflow) {
@ -37,7 +35,7 @@ const Render = ({ app, onClose }: Props) => {
initData(JSON.parse(JSON.stringify(v1Workflow2V2((app.modules || []) as any))));
})();
}
}, [app.modules, isV2Workflow, openConfirm]);
}, [app.modules, initData, isV2Workflow, openConfirm]);
const memoRender = useMemo(() => {
return <Flow Header={<Header app={app} onClose={onClose} />} />;

View File

@ -60,7 +60,7 @@ const EditForm = ({
const theme = useTheme();
const router = useRouter();
const { t } = useTranslation();
const { appDetail, updateAppDetail } = useAppStore();
const { appDetail, publishApp } = useAppStore();
const { loadAllDatasets, allDatasets } = useDatasetStore();
const { isPc, llmModelList } = useSystemStore();
@ -122,11 +122,10 @@ const EditForm = ({
mutationFn: async (data: AppSimpleEditFormType) => {
const { nodes, edges } = form2AppWorkflow(data);
await updateAppDetail(appDetail._id, {
modules: nodes,
await publishApp(appDetail._id, {
nodes,
edges,
type: AppTypeEnum.simple,
permission: undefined
type: AppTypeEnum.simple
});
},
successToast: t('common.Save Success'),

View File

@ -28,13 +28,13 @@ const TagsEditModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { appDetail } = useAppStore();
const { toast } = useToast();
const { replaceAppDetail } = useAppStore();
const { updateAppDetail } = useAppStore();
const [selectedTags, setSelectedTags] = useState<string[]>(appDetail?.teamTags || []);
// submit config
const { mutate: saveSubmitSuccess, isLoading: btnLoading } = useRequest({
mutationFn: async () => {
await replaceAppDetail(appDetail._id, {
await updateAppDetail(appDetail._id, {
teamTags: selectedTags
});
},

View File

@ -82,22 +82,6 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
const onCloseFlowEdit = useCallback(() => setCurrentTab(TabEnum.simpleEdit), [setCurrentTab]);
useEffect(() => {
const listen =
process.env.NODE_ENV === 'production'
? (e: any) => {
e.preventDefault();
e.returnValue = t('core.common.tip.leave page');
}
: () => {};
window.addEventListener('beforeunload', listen);
return () => {
window.removeEventListener('beforeunload', listen);
clearAppModules();
};
}, []);
useQuery([appId], () => loadAppDetail(appId, true), {
onError(err: any) {
toast({

View File

@ -1,91 +0,0 @@
import React from 'react';
import { Box, Flex, Button, Card } from '@chakra-ui/react';
import type { ShareAppItem } from '@/types/app';
import { useRouter } from 'next/router';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@/components/Avatar';
import MyTooltip from '@/components/MyTooltip';
const ShareModelList = ({
models = [],
onclickCollection
}: {
models: ShareAppItem[];
onclickCollection: (appId: string) => void;
}) => {
const router = useRouter();
return (
<>
{models.map((model) => (
<Card
key={model._id}
display={'flex'}
w={'100%'}
flexDirection={'column'}
p={4}
borderRadius={'md'}
border={'1px solid '}
userSelect={'none'}
boxShadow={'none'}
borderColor={'myGray.200'}
_hover={{
boxShadow: 'lg'
}}
>
<Flex alignItems={'center'}>
<Avatar
src={model.avatar}
w={['28px', '36px']}
h={['28px', '36px']}
borderRadius={'50%'}
/>
<Box fontWeight={'bold'} fontSize={'lg'} ml={5}>
{model.name}
</Box>
</Flex>
<MyTooltip label={model.intro}>
<Box
className={'textEllipsis3'}
flex={1}
my={4}
fontSize={'sm'}
wordBreak={'break-all'}
color={'blackAlpha.600'}
>
{model.intro || '这个应用还没有介绍~'}
</Box>
</MyTooltip>
<Flex justifyContent={'space-between'}>
<Flex
alignItems={'center'}
cursor={'pointer'}
color={model.isCollection ? 'primary.600' : 'blackAlpha.700'}
onClick={() => onclickCollection(model._id)}
>
<MyIcon
mr={1}
name={model.isCollection ? 'collectionSolid' : 'collectionLight'}
w={'16px'}
/>
{model.share.collection}
</Flex>
<Box>
<Button
size={'sm'}
variant={'whitePrimary'}
w={['60px', '70px']}
onClick={() => router.push(`/chat?appId=${model._id}`)}
>
</Button>
</Box>
</Flex>
</Card>
))}
</>
);
};
export default ShareModelList;

View File

@ -1,99 +0,0 @@
import React, { useState, useRef, useCallback } from 'react';
import { Box, Flex, Grid } from '@chakra-ui/react';
import { getShareModelList, triggerModelCollection } from '@/web/core/app/api';
import type { ShareAppItem } from '@/types/app';
import ShareModelList from './components/list';
import { serviceSideProps } from '@/web/common/utils/i18n';
import { usePagination } from '@fastgpt/web/hooks/usePagination';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
const modelList = () => {
const { Loading } = useLoading();
const lastSearch = useRef('');
const [searchText, setSearchText] = useState('');
/* 加载模型 */
const {
data: models,
isLoading,
Pagination,
getData,
pageNum
} = usePagination<ShareAppItem>({
api: getShareModelList,
pageSize: 24,
params: {
searchText
}
});
const onclickCollection = useCallback(
async (appId: string) => {
try {
await triggerModelCollection(appId);
getData(pageNum);
} catch (error) {
console.log(error);
}
},
[getData, pageNum]
);
return (
<Box px={[5, 10]} py={[4, 6]} position={'relative'} minH={'109vh'}>
<Flex alignItems={'center'} mb={2}>
<Box className={'textlg'} fontWeight={'bold'} fontSize={'3xl'}>
AI
</Box>
{/* <Box mt={[2, 0]} textAlign={'right'}>
<Input
w={['200px', '250px']}
size={'sm'}
value={searchText}
placeholder="搜索应用,回车确认"
onChange={(e) => setSearchText(e.target.value)}
onBlur={() => {
if (searchText === lastSearch.current) return;
getData(1);
lastSearch.current = searchText;
}}
onKeyDown={(e) => {
if (searchText === lastSearch.current) return;
if (e.key === 'Enter') {
getData(1);
lastSearch.current = searchText;
}
}}
/>
</Box> */}
</Flex>
<Grid
templateColumns={[
'repeat(1,1fr)',
'repeat(2,1fr)',
'repeat(3,1fr)',
'repeat(4,1fr)',
'repeat(5,1fr)'
]}
gridGap={4}
mt={4}
>
<ShareModelList models={models} onclickCollection={onclickCollection} />
</Grid>
<Flex mt={4} justifyContent={'center'}>
<Pagination />
</Flex>
<Loading loading={isLoading} />
</Box>
);
};
export async function getServerSideProps(content: any) {
return {
props: {
...(await serviceSideProps(content))
}
};
}
export default modelList;

View File

@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useMemo } from 'react';
import { Box, Flex, IconButton, useTheme, useDisclosure, Button } from '@chakra-ui/react';
import { PluginItemSchema } from '@fastgpt/global/core/plugin/type';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
@ -7,16 +7,14 @@ import { useCopyData } from '@/web/common/hooks/useCopyData';
import dynamic from 'next/dynamic';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@/components/MyTooltip';
import { filterExportModules, flowNode2StoreNodes } from '@/components/core/workflow/utils';
import { flowNode2StoreNodes } from '@/components/core/workflow/utils';
import { putUpdatePlugin } from '@/web/core/plugin/api';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { useToast } from '@fastgpt/web/hooks/useToast';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import {
getWorkflowStore,
useFlowProviderStore
} from '@/components/core/workflow/Flow/FlowProvider';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import {
checkWorkflowNodeAndConnection,
filterSensitiveNodesData
@ -51,107 +49,109 @@ const Header = ({ plugin, onClose }: Props) => {
}, [edges, onUpdateNodeError, t, toast]);
const { mutate: onclickSave, isLoading } = useRequest({
mutationFn: ({ nodes, edges }: { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] }) => {
return putUpdatePlugin({
id: plugin._id,
modules: nodes,
edges
});
},
successToast: '保存配置成功',
errorToast: '保存配置异常'
mutationFn: async () => {
const workflow = await flowData2StoreDataAndCheck();
if (workflow) {
await putUpdatePlugin({
id: plugin._id,
modules: workflow.nodes,
edges: workflow.edges
});
toast({
status: 'success',
title: t('common.Save Success')
});
}
}
});
return (
<>
<Flex
py={3}
px={[2, 5, 8]}
borderBottom={theme.borders.base}
alignItems={'center'}
userSelect={'none'}
>
<MyTooltip label={t('common.Back')} offset={[10, 10]}>
<IconButton
size={'smSquare'}
icon={<MyIcon name={'common/backLight'} w={'14px'} />}
variant={'whiteBase'}
aria-label={''}
onClick={() => {
onClose();
}}
/>
</MyTooltip>
<Box ml={[3, 5]} fontSize={['md', '2xl']} flex={1}>
{plugin.name}
</Box>
const onCopy = useCallback(async () => {
const data = await flowData2StoreDataAndCheck();
if (data) {
copyData(
JSON.stringify(
{
nodes: filterSensitiveNodesData(data.nodes),
edges: data.edges
},
null,
2
),
t('app.Export Config Successful')
);
}
}, [copyData, flowData2StoreDataAndCheck, t]);
<MyMenu
Button={
<IconButton
mr={[3, 5]}
icon={<MyIcon name={'more'} w={'14px'} p={2} />}
aria-label={''}
size={'sm'}
variant={'whitePrimary'}
/>
}
menuList={[
{ label: t('app.Import Configs'), icon: 'common/importLight', onClick: onOpenImport },
{
label: t('app.Export Configs'),
icon: 'export',
onClick: async () => {
const data = await flowData2StoreDataAndCheck();
if (data) {
copyData(
JSON.stringify(
{
nodes: filterSensitiveNodesData(data.nodes),
edges: data.edges
},
null,
2
),
t('app.Export Config Successful')
);
}
}
}
]}
/>
{/* <MyTooltip label={t('module.Preview Plugin')}>
<IconButton
mr={[3, 5]}
icon={<MyIcon name={'core/modules/previewLight'} w={['14px', '16px']} />}
size={'smSquare'}
aria-label={'save'}
variant={'whitePrimary'}
onClick={async () => {
const modules = await flowData2StoreDataAndCheck();
if (modules) {
setPreviewModules(modules);
}
}}
/>
</MyTooltip> */}
<Button
size={'sm'}
isLoading={isLoading}
leftIcon={<MyIcon name={'common/saveFill'} w={['14px', '16px']} />}
onClick={async () => {
const modules = await flowData2StoreDataAndCheck();
if (modules) {
onclickSave(modules);
}
}}
const Render = useMemo(() => {
return (
<>
<Flex
py={3}
px={[2, 5, 8]}
borderBottom={theme.borders.base}
alignItems={'center'}
userSelect={'none'}
>
{t('common.Save')}
</Button>
</Flex>
{isOpenImport && <ImportSettings onClose={onCloseImport} />}
</>
);
<MyTooltip label={t('common.Back')} offset={[10, 10]}>
<IconButton
size={'smSquare'}
icon={<MyIcon name={'common/backLight'} w={'14px'} />}
variant={'whiteBase'}
aria-label={''}
onClick={() => {
onClose();
}}
/>
</MyTooltip>
<Box ml={[3, 5]} fontSize={['md', '2xl']} flex={1}>
{plugin.name}
</Box>
<MyMenu
Button={
<IconButton
mr={[3, 5]}
icon={<MyIcon name={'more'} w={'14px'} p={2} />}
aria-label={''}
size={'sm'}
variant={'whitePrimary'}
/>
}
menuList={[
{ label: t('app.Import Configs'), icon: 'common/importLight', onClick: onOpenImport },
{
label: t('app.Export Configs'),
icon: 'export',
onClick: onCopy
}
]}
/>
<Button
size={'sm'}
isLoading={isLoading}
leftIcon={<MyIcon name={'common/saveFill'} w={['14px', '16px']} />}
onClick={onclickSave}
>
{t('common.Save')}
</Button>
</Flex>
{isOpenImport && <ImportSettings onClose={onCloseImport} />}
</>
);
}, [
isLoading,
isOpenImport,
onClose,
onCloseImport,
onCopy,
onOpenImport,
onclickSave,
plugin.name,
t,
theme.borders.base
]);
return Render;
};
export default React.memo(Header);

View File

@ -13,6 +13,7 @@ import { getErrText } from '@fastgpt/global/common/error/utils';
import { useTranslation } from 'next-i18next';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { v1Workflow2V2 } from '@/web/core/workflow/adapt';
import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload';
type Props = { pluginId: string };
@ -42,18 +43,16 @@ const Render = ({ pluginId }: Props) => {
'检测到您的高级编排为旧版,系统将为您自动格式化成新版工作流。\n\n由于版本差异较大会导致许多工作流无法正常排布请重新手动连接工作流。如仍异常可尝试删除对应节点后重新添加。\n\n你可以直接点击测试进行调试无需点击保存点击保存为新版工作流。'
});
const workflowStringData = JSON.stringify({
nodes: pluginDetail?.modules || [],
edges: pluginDetail?.edges || []
});
useEffect(() => {
if (isV2Workflow) {
initData(
JSON.parse(
JSON.stringify({
nodes: pluginDetail?.modules || [],
edges: pluginDetail?.edges || []
})
)
);
initData(JSON.parse(workflowStringData));
}
}, [isV2Workflow, pluginDetail?.edges, pluginDetail?.modules]);
}, [initData, isV2Workflow, workflowStringData]);
useEffect(() => {
if (!isV2Workflow && pluginDetail) {
@ -61,7 +60,11 @@ const Render = ({ pluginId }: Props) => {
initData(JSON.parse(JSON.stringify(v1Workflow2V2((pluginDetail.modules || []) as any))));
})();
}
}, [isV2Workflow, openConfirm, pluginDetail]);
}, [initData, isV2Workflow, openConfirm, pluginDetail]);
useBeforeunload({
tip: t('core.common.tip.leave page')
});
return pluginDetail ? (
<>

View File

@ -0,0 +1,30 @@
import { jsonRes } from '@fastgpt/service/common/response';
import type { NextApiResponse, NextApiHandler, NextApiRequest } from 'next';
import { connectToDatabase } from '../mongo';
import { withNextCors } from '@fastgpt/service/common/middle/cors';
export const NextAPI = (...args: NextApiHandler[]): NextApiHandler => {
return async function api(req: NextApiRequest, res: NextApiResponse) {
try {
await Promise.all([withNextCors(req, res), connectToDatabase()]);
let response = null;
for (const handler of args) {
response = await handler(req, res);
}
if (!res.writableFinished) {
return jsonRes(res, {
code: 200,
data: response
});
}
} catch (error) {
return jsonRes(res, {
code: 500,
error,
url: req.url
});
}
};
};

View File

@ -1,9 +1,7 @@
import { GET, POST, DELETE, PUT } from '@/web/common/api/request';
import type { AppDetailType, AppListItemType } from '@fastgpt/global/core/app/type.d';
import { RequestPaging } from '@/types/index';
import { addDays } from 'date-fns';
import type { GetAppChatLogsParams } from '@/global/core/api/appReq.d';
import type { CreateAppParams, AppUpdateParams } from '@fastgpt/global/core/app/api.d';
import { AppUpdateParams, CreateAppParams } from '@/global/core/app/api';
/**
*
@ -31,32 +29,6 @@ export const getModelById = (id: string) => GET<AppDetailType>(`/core/app/detail
*/
export const putAppById = (id: string, data: AppUpdateParams) =>
PUT(`/core/app/update?appId=${id}`, data);
export const replaceAppById = (id: string, data: AppUpdateParams) =>
PUT(`/core/app/updateTeamTasg?appId=${id}`, data);
// updateTeamTasg
export const putAppTagsById = (id: string, data: AppUpdateParams) =>
PUT(`/core/app/updateTeamTasg?appId=${id}`, data);
/* 共享市场 */
/**
*
*/
export const getShareModelList = (data: { searchText?: string } & RequestPaging) =>
POST(`/core/app/share/getModels`, data);
/**
* /
*/
export const triggerModelCollection = (appId: string) =>
POST<number>(`/core/app/share/collection?appId=${appId}`);
// ====================== data
export const getAppTotalUsage = (data: { appId: string }) =>
POST<{ date: String; total: number }[]>(`/core/app/data/totalUsage`, {
...data,
start: addDays(new Date(), -13),
end: addDays(new Date(), 1)
}).then((res) => (res.length === 0 ? [{ date: new Date(), total: 0 }] : res));
// =================== chat logs
export const getAppChatLogs = (data: GetAppChatLogsParams) => POST(`/core/app/getChatLogs`, data);

View File

@ -1,10 +1,12 @@
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import { getMyApps, getModelById, putAppById, replaceAppById } from '@/web/core/app/api';
import { getMyApps, getModelById, putAppById } from '@/web/core/app/api';
import { defaultApp } from '@/constants/app';
import type { AppUpdateParams } from '@fastgpt/global/core/app/api.d';
import type { AppUpdateParams } from '@/global/core/app/api.d';
import { AppDetailType, AppListItemType } from '@fastgpt/global/core/app/type.d';
import { PostPublishAppProps } from '@/global/core/app/api';
import { postPublishApp } from '../versionApi';
type State = {
myApps: AppListItemType[];
@ -12,7 +14,7 @@ type State = {
appDetail: AppDetailType;
loadAppDetail: (id: string, init?: boolean) => Promise<AppDetailType>;
updateAppDetail(appId: string, data: AppUpdateParams): Promise<void>;
replaceAppDetail(appId: string, data: AppUpdateParams): Promise<void>;
publishApp(appId: string, data: PostPublishAppProps): Promise<void>;
clearAppModules(): void;
};
@ -44,19 +46,22 @@ export const useAppStore = create<State>()(
set((state) => {
state.appDetail = {
...state.appDetail,
...data
...data,
modules: data?.nodes || state.appDetail.modules
};
});
},
async replaceAppDetail(appId: string, data: AppUpdateParams) {
await replaceAppById(appId, { ...get().appDetail, ...data });
async publishApp(appId: string, data: PostPublishAppProps) {
await postPublishApp(appId, data);
set((state) => {
state.appDetail = {
...state.appDetail,
...data
...data,
modules: data?.nodes || state.appDetail.modules
};
});
},
clearAppModules() {
set((state) => {
state.appDetail = {

View File

@ -0,0 +1,5 @@
import { PostPublishAppProps } from '@/global/core/app/api';
import { GET, POST, DELETE, PUT } from '@/web/common/api/request';
export const postPublishApp = (appId: string, data: PostPublishAppProps) =>
POST(`/core/app/version/publish?appId=${appId}`, data);

View File

@ -15,7 +15,7 @@ import {
FlowNodeTemplateType,
StoreNodeItemType
} from '@fastgpt/global/core/workflow/type';
import { VARIABLE_NODE_ID } from './constants';
import { VARIABLE_NODE_ID } from '@fastgpt/global/core/workflow/constants';
import { getHandleId, splitGuideModule } from '@fastgpt/global/core/workflow/utils';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { LLMModelTypeEnum } from '@fastgpt/global/core/ai/constants';

View File

@ -2,34 +2,47 @@ import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants
export const FlowValueTypeMap = {
[WorkflowIOValueTypeEnum.string]: {
handlerStyle: {
borderColor: '#36ADEF'
},
label: 'core.module.valueType.string',
label: 'string',
value: WorkflowIOValueTypeEnum.string,
description: ''
},
[WorkflowIOValueTypeEnum.number]: {
handlerStyle: {
borderColor: '#FB7C3C'
},
label: 'core.module.valueType.number',
label: 'number',
value: WorkflowIOValueTypeEnum.number,
description: ''
},
[WorkflowIOValueTypeEnum.boolean]: {
handlerStyle: {
borderColor: '#E7D118'
},
label: 'core.module.valueType.boolean',
label: 'boolean',
value: WorkflowIOValueTypeEnum.boolean,
description: ''
},
[WorkflowIOValueTypeEnum.object]: {
label: 'object',
value: WorkflowIOValueTypeEnum.object,
description: ''
},
[WorkflowIOValueTypeEnum.arrayString]: {
label: 'array<string>',
value: WorkflowIOValueTypeEnum.arrayString,
description: ''
},
[WorkflowIOValueTypeEnum.arrayNumber]: {
label: 'array<number>',
value: WorkflowIOValueTypeEnum.arrayNumber,
description: ''
},
[WorkflowIOValueTypeEnum.arrayBoolean]: {
label: 'array<boolean>',
value: WorkflowIOValueTypeEnum.arrayBoolean,
description: ''
},
[WorkflowIOValueTypeEnum.arrayObject]: {
label: 'array<object>',
value: WorkflowIOValueTypeEnum.arrayObject,
description: ''
},
[WorkflowIOValueTypeEnum.chatHistory]: {
handlerStyle: {
borderColor: '#00A9A6'
},
label: 'core.module.valueType.chatHistory',
label: '历史记录',
value: WorkflowIOValueTypeEnum.chatHistory,
description: `{
obj: System | Human | AI;
@ -37,10 +50,7 @@ export const FlowValueTypeMap = {
}[]`
},
[WorkflowIOValueTypeEnum.datasetQuote]: {
handlerStyle: {
borderColor: '#A558C9'
},
label: 'core.module.valueType.datasetQuote',
label: '知识库引用',
value: WorkflowIOValueTypeEnum.datasetQuote,
description: `{
id: string;
@ -52,42 +62,22 @@ export const FlowValueTypeMap = {
a: string
}[]`
},
[WorkflowIOValueTypeEnum.any]: {
handlerStyle: {
borderColor: '#9CA2A8'
},
label: 'core.module.valueType.any',
value: WorkflowIOValueTypeEnum.any,
description: ''
},
[WorkflowIOValueTypeEnum.selectApp]: {
handlerStyle: {
borderColor: '#6a6efa'
},
label: 'core.module.valueType.selectApp',
label: '选择应用',
value: WorkflowIOValueTypeEnum.selectApp,
description: ''
},
[WorkflowIOValueTypeEnum.selectDataset]: {
handlerStyle: {
borderColor: '#21ba45'
},
label: 'core.module.valueType.selectDataset',
label: '选择知识库',
value: WorkflowIOValueTypeEnum.selectDataset,
description: ''
},
[WorkflowIOValueTypeEnum.tools]: {
handlerStyle: {
borderColor: '#21ba45'
},
label: 'core.module.valueType.tools',
value: WorkflowIOValueTypeEnum.tools,
[WorkflowIOValueTypeEnum.any]: {
label: 'any',
value: WorkflowIOValueTypeEnum.any,
description: ''
},
[WorkflowIOValueTypeEnum.dynamic]: {
handlerStyle: {
borderColor: '#9CA2A8'
},
label: '动态数据',
value: WorkflowIOValueTypeEnum.any,
description: ''

View File

@ -1 +0,0 @@
export const VARIABLE_NODE_ID = 'VARIABLE_NODE_ID';

View File

@ -14,7 +14,7 @@ import { EmptyNode } from '@fastgpt/global/core/workflow/template/system/emptyNo
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { systemConfigNode2VariableNode } from './adapt';
import { VARIABLE_NODE_ID } from './constants';
import { VARIABLE_NODE_ID } from '@fastgpt/global/core/workflow/constants';
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
export const nodeTemplate2FlowNode = ({