Commit 4ab4ad23 by 赵月辉

fix: 修复类型错误并通过 pnpm build 验证

- Approve.ts 改用官方新版 createDatasetData 替代 insertData2Dataset
- team.ts 补全 lufa 定制 authTeamSpaceToken 函数及 plusRequest 路径
- 删除 0 引用的 redis 死代码文件 (redisConnection/redisCache)
- tsconfig exclude plugins 目录避免编译期依赖缺失
- 修复 lint errors: {} 类型、react-hooks/set-state-in-effect
- 清理 lufa 定制中已被官方新版取代的冗余文件
parent fc954464
......@@ -95,6 +95,7 @@ export type FastGPTFeConfigsType = {
loginGuideDocUrl?: string;
openAPIDocUrl?: string;
submitPluginRequestUrl?: string;
systemPluginCourseUrl?: string;
appTemplateCourse?: string;
customApiDomain?: string;
customSharePageDomain?: string;
......
......@@ -3,6 +3,10 @@ import { type UpdateClbPermissionProps } from '../../support/permission/collabor
export type UpdateAppCollaboratorBody = UpdateClbPermissionProps & {
appId: string;
groups?: string[];
members?: string[];
orgs?: string[];
permission?: number;
};
export type AppCollaboratorDeleteParams = {
......
......@@ -3,6 +3,10 @@ import type { RequireOnlyOne } from '../../common/type/utils';
export type UpdateDatasetCollaboratorBody = UpdateClbPermissionProps & {
datasetId: string;
groups?: string[];
members?: string[];
orgs?: string[];
permission?: number;
};
export type DatasetCollaboratorDeleteParams = {
......
......@@ -2,13 +2,18 @@ export const TeamCollectionName = 'teams';
export const TeamMemberCollectionName = 'team_members';
export enum TeamMemberRoleEnum {
owner = 'owner'
owner = 'owner',
admin = 'admin'
}
export const TeamMemberRoleMap = {
[TeamMemberRoleEnum.owner]: {
value: TeamMemberRoleEnum.owner,
label: 'user.team.role.Owner'
},
[TeamMemberRoleEnum.admin]: {
value: TeamMemberRoleEnum.admin,
label: 'user.team.role.Admin'
}
};
......
......@@ -58,6 +58,7 @@ export const TeamTmbItemSchema = ThidPartyAccountSchema.extend({
avatar: z.string(),
balance: z.number().optional(),
tmbId: z.string(),
teamDomain: z.string().optional(),
role: z.enum(TeamMemberRoleEnum),
status: z.enum(TeamMemberStatusEnum),
notificationAccount: z.string().optional(),
......
......@@ -42,6 +42,7 @@ export const UserSchema = z.object({
promotionRate: z.number(),
team: TeamTmbItemSchema,
permission: z.instanceof(TeamPermission),
notificationAccount: z.string().optional(),
contact: z.string().optional(),
tags: z.array(UserTagsSchema).optional()
});
......
import { redisConnect } from './redisConnection';
import { performance } from 'perf_hooks';
const startPerfTimer = (): number => {
return performance.now();
};
const endPerfTimer = (): number => {
return performance.now();
};
const calculatePerformance = (startTime: number, endTime: number): void => {
console.log(`Response took ${endTime - startTime} milliseconds`);
};
export const fetchCache = async (
key: string,
fetchData: () => Promise<unknown | null | undefined>,
expiresIn: number
) => {
startPerfTimer();
const cachedData = await getKey(key);
if (cachedData) {
console.log('Fetched from cache');
calculatePerformance(startPerfTimer(), endPerfTimer());
return cachedData;
}
console.log('Fetched from API');
calculatePerformance(startPerfTimer(), endPerfTimer());
return setValue(key, fetchData, expiresIn);
};
const getKey = async <T>(key: string): Promise<T | null> => {
const result = await redisConnect.get(key);
if (result) return JSON.parse(result);
endPerfTimer();
return null;
};
const setValue = async <T>(
key: string,
fetchData: () => Promise<T>,
expiresIn: number
): Promise<T> => {
const setValue = await fetchData();
await redisConnect.set(key, JSON.stringify(setValue), 'EX', expiresIn);
endPerfTimer();
return setValue;
};
import Redis from 'ioredis';
const REDIS_URL = process.env.REDIS_URL ?? 'redis://localhost:6379';
export const redisConnect = new Redis(REDIS_URL);
import { RunToolWithStream } from '@fastgpt-sdk/plugin';
import { PluginSourceEnum } from '@fastgpt/global/core/app/plugin/constants';
import { pluginClient, BASE_URL, TOKEN } from '../../../thirdProvider/fastgptPlugin';
export async function APIGetSystemToolList() {
// 检查插件服务是否可用
if (!BASE_URL) {
console.log('Plugin service not configured, returning empty tool list');
return [];
}
try {
const res = await pluginClient.tool.list();
if (res.status === 200) {
return res.body.map((item) => {
return {
...item,
id: `${PluginSourceEnum.systemTool}-${item.id}`,
parentId: item.parentId ? `${PluginSourceEnum.systemTool}-${item.parentId}` : undefined,
avatar:
item.avatar && item.avatar.startsWith('/imgs/tools/')
? `/api/system/pluginImgs/${item.avatar.replace('/imgs/tools/', '')}`
: item.avatar
};
});
}
return Promise.reject(res.body);
} catch (error) {
console.error('Plugin service error:', error);
return [];
}
}
const runToolInstance = BASE_URL
? new RunToolWithStream({
baseUrl: BASE_URL,
token: TOKEN
})
: null;
export const APIRunSystemTool = async (params: {
toolId: string;
inputs: Record<string, any>;
systemVar: {
user: {
id: string;
username: string;
contact: string;
membername: string;
teamName: string;
teamId: string;
name: string;
};
app: {
id: string;
name: string;
};
tool: {
id: string;
version: string;
};
time: string;
};
onMessage: (message: { type: string; content: string }) => void;
}) => {
if (!runToolInstance) {
throw new Error('Plugin service not configured');
}
return runToolInstance.run(params);
};
......@@ -131,7 +131,6 @@ export const datasetDeleteProcessor: Processor<DatasetDeleteJobData> = async (jo
try {
// 1. 查找知识库及其所有子知识库
const datasets = await findDatasetAndAllChildren({
teamId,
datasetId,
fields: '_id teamId avatar'
});
......
import { createChatCompletion } from '../../../../ai/config';
import { filterGPTMessageByMaxContext, loadRequestMessages } from '../../../../chat/utils';
import {
type ChatCompletion,
type ChatCompletionMessageToolCall,
type StreamChatType,
type ChatCompletionToolMessageParam,
type ChatCompletionMessageParam,
type ChatCompletionTool,
type CompletionFinishReason
} from '@fastgpt/global/core/ai/type';
import { type NextApiResponse } from 'next';
import { responseWriteController } from '../../../../../common/response';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils';
import { ChatCompletionRequestMessageRoleEnum } from '@fastgpt/global/core/ai/constants';
import { dispatchWorkFlow } from '../../index';
import { type DispatchToolModuleProps, type RunToolResponse, type ToolNodeItemType } from './type';
import json5 from 'json5';
import { type DispatchFlowResponse, type WorkflowResponseType } from '../../type';
import { countGptMessagesTokens } from '../../../../../common/string/tiktoken/index';
import { GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt';
import { type AIChatItemType } from '@fastgpt/global/core/chat/type';
import { formatToolResponse, initToolCallEdges, initToolNodes } from './utils';
import {
computedMaxToken,
llmCompletionsBodyFormat,
removeDatasetCiteText,
parseLLMStreamResponse
} from '../../../../ai/utils';
import { getNanoid, sliceStrStartEnd } from '@fastgpt/global/common/string/tools';
import { toolValueTypeList, valueTypeJsonSchemaMap } from '@fastgpt/global/core/workflow/constants';
import { type WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
import { getErrText } from '@fastgpt/global/common/error/utils';
type ToolRunResponseType = {
toolRunResponse?: DispatchFlowResponse;
toolMsgParams: ChatCompletionToolMessageParam;
}[];
/*
调用思路:
先Check 是否是交互节点触发
交互模式:
1. 从缓存中获取工作流运行数据
2. 运行工作流
3. 检测是否有停止信号或交互响应
- 无:汇总结果,递归运行工具
- 有:缓存结果,结束调用
非交互模式:
1. 组合 tools
2. 过滤 messages
3. Load request llm messages: system prompt, histories, human question, (assistant responses, tool responses, assistant responses....)
4. 请求 LLM 获取结果
- 有工具调用
1. 批量运行工具的工作流,获取结果(工作流原生结果,工具执行结果)
2. 合并递归中,所有工具的原生运行结果
3. 组合 assistants tool 响应
4. 组合本次 request 和 llm response 的 messages,并计算出消耗的 tokens
5. 组合本次 request、llm response 和 tool response 结果
6. 组合本次的 assistant responses: history assistant + tool assistant + tool child assistant
7. 判断是否还有停止信号或交互响应
- 无:递归运行工具
- 有:缓存结果,结束调用
- 无工具调用
1. 汇总结果,递归运行工具
2. 计算 completeMessages 和 tokens 后返回。
交互节点额外缓存结果包括:
1. 入口的节点 id
2. toolCallId: 本次工具调用的 ID,可以找到是调用了哪个工具,入口并不会记录工具的 id
3. messages:本次递归中,assistants responses 和 tool responses
*/
export const runToolWithToolChoice = async (
props: DispatchToolModuleProps & {
maxRunToolTimes: number;
},
response?: RunToolResponse
): Promise<RunToolResponse> => {
const {
messages,
toolNodes,
toolModel,
maxRunToolTimes,
interactiveEntryToolParams,
...workflowProps
} = props;
let {
res,
requestOrigin,
runtimeNodes,
runtimeEdges,
stream,
retainDatasetCite = true,
externalProvider,
workflowStreamResponse,
params: {
temperature,
maxToken,
aiChatVision,
aiChatTopP,
aiChatStopSign,
aiChatResponseFormat,
aiChatJsonSchema,
aiChatReasoning
}
} = workflowProps;
aiChatReasoning = !!aiChatReasoning && !!toolModel.reasoning;
if (maxRunToolTimes <= 0 && response) {
return response;
}
// Interactive
if (interactiveEntryToolParams) {
initToolNodes(runtimeNodes, interactiveEntryToolParams.entryNodeIds);
initToolCallEdges(runtimeEdges, interactiveEntryToolParams.entryNodeIds);
// Run entry tool
const toolRunResponse = await dispatchWorkFlow({
...workflowProps,
isToolCall: true
});
const stringToolResponse = formatToolResponse(toolRunResponse.toolResponses);
// Response to frontend
workflowStreamResponse?.({
event: SseResponseEventEnum.toolResponse,
data: {
tool: {
id: interactiveEntryToolParams.toolCallId,
toolName: '',
toolAvatar: '',
params: '',
response: sliceStrStartEnd(stringToolResponse, 5000, 5000)
}
}
});
// Check stop signal
const hasStopSignal = toolRunResponse.flowResponses?.some((item) => item.toolStop);
// Check interactive response(Only 1 interaction is reserved)
const workflowInteractiveResponse = toolRunResponse.workflowInteractiveResponse;
const requestMessages = [
...messages,
...interactiveEntryToolParams.memoryMessages.map((item) =>
item.role === 'tool' && item.tool_call_id === interactiveEntryToolParams.toolCallId
? {
...item,
content: stringToolResponse
}
: item
)
];
if (hasStopSignal || workflowInteractiveResponse) {
// Get interactive tool data
const toolWorkflowInteractiveResponse: WorkflowInteractiveResponseType | undefined =
workflowInteractiveResponse
? {
...workflowInteractiveResponse,
toolParams: {
entryNodeIds: workflowInteractiveResponse.entryNodeIds,
toolCallId: interactiveEntryToolParams.toolCallId,
memoryMessages: interactiveEntryToolParams.memoryMessages
}
}
: undefined;
return {
dispatchFlowResponse: [toolRunResponse],
toolNodeInputTokens: 0,
toolNodeOutputTokens: 0,
completeMessages: requestMessages,
assistantResponses: toolRunResponse.assistantResponses,
runTimes: toolRunResponse.runTimes,
toolWorkflowInteractiveResponse
};
}
return runToolWithToolChoice(
{
...props,
interactiveEntryToolParams: undefined,
maxRunToolTimes: maxRunToolTimes - 1,
// Rewrite toolCall messages
messages: requestMessages
},
{
dispatchFlowResponse: [toolRunResponse],
toolNodeInputTokens: 0,
toolNodeOutputTokens: 0,
assistantResponses: toolRunResponse.assistantResponses,
runTimes: toolRunResponse.runTimes
}
);
}
// ------------------------------------------------------------
const assistantResponses = response?.assistantResponses || [];
const tools: ChatCompletionTool[] = toolNodes.map((item) => {
if (item.jsonSchema) {
return {
type: 'function',
function: {
name: item.nodeId,
description: item.intro || item.name,
parameters: item.jsonSchema
}
};
}
const properties: Record<
string,
{
type: string;
description: string;
enum?: string[];
required?: boolean;
items?: {
type: string;
};
}
> = {};
item.toolParams.forEach((item) => {
const jsonSchema = item.valueType
? valueTypeJsonSchemaMap[item.valueType] || toolValueTypeList[0].jsonSchema
: toolValueTypeList[0].jsonSchema;
properties[item.key] = {
...jsonSchema,
description: item.toolDescription || '',
enum: item.enum?.split('\n').filter(Boolean) || undefined
};
});
return {
type: 'function',
function: {
name: item.nodeId,
description: item.toolDescription || item.intro || item.name,
parameters: {
type: 'object',
properties,
required: item.toolParams.filter((item) => item.required).map((item) => item.key)
}
}
};
});
const max_tokens = computedMaxToken({
model: toolModel,
maxToken,
min: 100
});
// Filter histories by maxToken
const filterMessages = (
await filterGPTMessageByMaxContext({
messages,
maxContext: toolModel.maxContext - (max_tokens || 0) // filter token. not response maxToken
})
).map((item) => {
if (item.role === 'assistant' && item.tool_calls) {
return {
...item,
tool_calls: item.tool_calls.map((tool) => ({
id: tool.id,
type: tool.type,
function: tool.function
}))
};
}
return item;
});
const [requestMessages] = await Promise.all([
loadRequestMessages({
messages: filterMessages,
useVision: toolModel.vision && aiChatVision,
origin: requestOrigin
})
]);
const requestBody = llmCompletionsBodyFormat(
{
model: toolModel.model,
stream,
messages: requestMessages,
tools,
tool_choice: 'auto',
parallel_tool_calls: true,
temperature,
max_tokens,
top_p: aiChatTopP,
stop: aiChatStopSign,
response_format: {
type: aiChatResponseFormat as any,
json_schema: aiChatJsonSchema
}
},
toolModel
);
// console.log(JSON.stringify(requestBody, null, 2), '==requestMessages');
/* Run llm */
const {
response: aiResponse,
isStreamResponse,
getEmptyResponseTip
} = await createChatCompletion({
body: requestBody,
userKey: externalProvider.openaiAccount,
options: {
headers: {
Accept: 'application/json, text/plain, */*'
}
}
});
let { reasoningContent, answer, toolCalls, finish_reason, inputTokens, outputTokens } =
await (async () => {
if (isStreamResponse) {
if (!res || res.closed) {
return {
reasoningContent: '',
answer: '',
toolCalls: [],
finish_reason: 'close' as const,
inputTokens: 0,
outputTokens: 0
};
}
const result = await streamResponse({
res,
workflowStreamResponse,
toolNodes,
stream: aiResponse,
aiChatReasoning,
retainDatasetCite
});
return {
reasoningContent: result.reasoningContent,
answer: result.answer,
toolCalls: result.toolCalls,
finish_reason: result.finish_reason,
inputTokens: result.usage.prompt_tokens,
outputTokens: result.usage.completion_tokens
};
} else {
const result = aiResponse as ChatCompletion;
const finish_reason = result.choices?.[0]?.finish_reason as CompletionFinishReason;
const calls = result.choices?.[0]?.message?.tool_calls || [];
const answer = result.choices?.[0]?.message?.content || '';
// @ts-ignore
const reasoningContent = result.choices?.[0]?.message?.reasoning_content || '';
const usage = result.usage;
const formatReasoningContent = removeDatasetCiteText(reasoningContent, retainDatasetCite);
const formatAnswer = removeDatasetCiteText(answer, retainDatasetCite);
if (aiChatReasoning && reasoningContent) {
workflowStreamResponse?.({
event: SseResponseEventEnum.fastAnswer,
data: textAdaptGptResponse({
reasoning_content: formatReasoningContent
})
});
}
// 格式化 toolCalls
const toolCalls = calls.map((tool) => {
const toolNode = toolNodes.find((item) => item.nodeId === tool.function?.name);
// 不支持 stream 模式的模型的这里需要补一个响应给客户端
workflowStreamResponse?.({
event: SseResponseEventEnum.toolCall,
data: {
tool: {
id: tool.id,
toolName: toolNode?.name || '',
toolAvatar: toolNode?.avatar || '',
functionName: tool.function.name,
params: tool.function?.arguments ?? '',
response: ''
}
}
});
return {
...tool,
toolName: toolNode?.name || '',
toolAvatar: toolNode?.avatar || ''
};
});
if (answer) {
workflowStreamResponse?.({
event: SseResponseEventEnum.fastAnswer,
data: textAdaptGptResponse({
text: formatAnswer
})
});
}
return {
reasoningContent: formatReasoningContent,
answer: formatAnswer,
toolCalls: toolCalls,
finish_reason,
inputTokens: usage?.prompt_tokens,
outputTokens: usage?.completion_tokens
};
}
})();
if (!answer && !reasoningContent && toolCalls.length === 0) {
return Promise.reject(getEmptyResponseTip());
}
/* Run the selected tool by LLM.
Since only reference parameters are passed, if the same tool is run in parallel, it will get the same run parameters
*/
const toolsRunResponse: ToolRunResponseType = [];
for await (const tool of toolCalls) {
try {
const toolNode = toolNodes.find((item) => item.nodeId === tool.function?.name);
if (!toolNode) continue;
const startParams = (() => {
try {
return json5.parse(tool.function.arguments);
} catch (error) {
return {};
}
})();
initToolNodes(runtimeNodes, [toolNode.nodeId], startParams);
const toolRunResponse = await dispatchWorkFlow({
...workflowProps,
isToolCall: true
});
const stringToolResponse = formatToolResponse(toolRunResponse.toolResponses);
const toolMsgParams: ChatCompletionToolMessageParam = {
tool_call_id: tool.id,
role: ChatCompletionRequestMessageRoleEnum.Tool,
name: tool.function.name,
content: stringToolResponse
};
workflowStreamResponse?.({
event: SseResponseEventEnum.toolResponse,
data: {
tool: {
id: tool.id,
toolName: '',
toolAvatar: '',
params: '',
response: sliceStrStartEnd(stringToolResponse, 5000, 5000)
}
}
});
toolsRunResponse.push({
toolRunResponse,
toolMsgParams
});
} catch (error) {
const err = getErrText(error);
workflowStreamResponse?.({
event: SseResponseEventEnum.toolResponse,
data: {
tool: {
id: tool.id,
toolName: '',
toolAvatar: '',
params: '',
response: sliceStrStartEnd(err, 5000, 5000)
}
}
});
toolsRunResponse.push({
toolRunResponse: undefined,
toolMsgParams: {
tool_call_id: tool.id,
role: ChatCompletionRequestMessageRoleEnum.Tool,
name: tool.function.name,
content: sliceStrStartEnd(err, 5000, 5000)
}
});
}
}
const flatToolsResponseData = toolsRunResponse
.map((item) => item.toolRunResponse)
.flat()
.filter(Boolean) as DispatchFlowResponse[];
// concat tool responses
const dispatchFlowResponse = response
? response.dispatchFlowResponse.concat(flatToolsResponseData)
: flatToolsResponseData;
if (toolCalls.length > 0) {
// Run the tool, combine its results, and perform another round of AI calls
const assistantToolMsgParams: ChatCompletionMessageParam[] = [
...(answer || reasoningContent
? [
{
role: ChatCompletionRequestMessageRoleEnum.Assistant as 'assistant',
content: answer,
reasoning_text: reasoningContent
}
]
: []),
{
role: ChatCompletionRequestMessageRoleEnum.Assistant,
tool_calls: toolCalls
}
];
/*
...
user
assistant: tool data
*/
const concatToolMessages = [
...requestMessages,
...assistantToolMsgParams
] as ChatCompletionMessageParam[];
// Only toolCall tokens are counted here, Tool response tokens count towards the next reply
inputTokens = inputTokens || (await countGptMessagesTokens(requestMessages, tools));
outputTokens = outputTokens || (await countGptMessagesTokens(assistantToolMsgParams));
/*
...
user
assistant: tool data
tool: tool response
*/
const completeMessages = [
...concatToolMessages,
...toolsRunResponse.map((item) => item?.toolMsgParams)
];
/*
Get tool node assistant response
history assistant
current tool assistant
tool child assistant
*/
const toolNodeAssistant = GPTMessages2Chats([
...assistantToolMsgParams,
...toolsRunResponse.map((item) => item?.toolMsgParams)
])[0] as AIChatItemType;
const toolChildAssistants = flatToolsResponseData
.map((item) => item.assistantResponses)
.flat()
.filter((item) => item.type !== ChatItemValueTypeEnum.interactive); // 交互节点留着下次记录
const toolNodeAssistants = [
...assistantResponses,
...toolNodeAssistant.value,
...toolChildAssistants
];
const runTimes =
(response?.runTimes || 0) +
flatToolsResponseData.reduce((sum, item) => sum + item.runTimes, 0);
const toolNodeInputTokens = response ? response.toolNodeInputTokens + inputTokens : inputTokens;
const toolNodeOutputTokens = response
? response.toolNodeOutputTokens + outputTokens
: outputTokens;
// Check stop signal
const hasStopSignal = flatToolsResponseData.some(
(item) => !!item.flowResponses?.find((item) => item.toolStop)
);
// Check interactive response(Only 1 interaction is reserved)
const workflowInteractiveResponseItem = toolsRunResponse.find(
(item) => item.toolRunResponse?.workflowInteractiveResponse
);
// Check finish_reason: if it's 'stop' or 'length', we should stop recursive calls
const shouldStopByFinishReason = finish_reason === 'stop' || finish_reason === 'length';
// Check for duplicate tool calls (same tool name and arguments) to prevent infinite loops
// This prevents MCP tools from being called repeatedly with the same parameters
const checkDuplicateToolCalls = () => {
if (toolCalls.length === 0) return false;
// Get the last assistant message with tool calls from requestMessages
// (checking history to see if model is repeating the same tool call)
let lastToolCalls: Array<{ name: string; arguments: string }> = [];
for (let i = requestMessages.length - 1; i >= 0; i--) {
const msg = requestMessages[i];
if (msg.role === 'assistant' && msg.tool_calls && msg.tool_calls.length > 0) {
lastToolCalls = msg.tool_calls
.map((tc) => ({
name: tc.function?.name || '',
arguments: tc.function?.arguments || ''
}))
.filter((tc) => tc.name);
break;
}
}
if (lastToolCalls.length === 0) return false;
// Check if current tool calls are identical to the last ones
const currentToolCallKeys = toolCalls
.map((tc) => ({
name: tc.function?.name || '',
arguments: tc.function?.arguments || ''
}))
.filter((tc) => tc.name);
// If all current tool calls match the last ones exactly, it's a duplicate
if (
currentToolCallKeys.length === lastToolCalls.length &&
currentToolCallKeys.every((current, index) => {
const last = lastToolCalls[index];
return last && current.name === last.name && current.arguments === last.arguments;
})
) {
return true;
}
return false;
};
const hasDuplicateToolCalls = checkDuplicateToolCalls();
if (
hasStopSignal ||
workflowInteractiveResponseItem ||
shouldStopByFinishReason ||
hasDuplicateToolCalls
) {
// Get interactive tool data
const workflowInteractiveResponse =
workflowInteractiveResponseItem?.toolRunResponse?.workflowInteractiveResponse;
// Flashback traverses completeMessages, intercepting messages that know the first user
const firstUserIndex = completeMessages.findLastIndex((item) => item.role === 'user');
const newMessages = completeMessages.slice(firstUserIndex + 1);
const toolWorkflowInteractiveResponse: WorkflowInteractiveResponseType | undefined =
workflowInteractiveResponse
? {
...workflowInteractiveResponse,
toolParams: {
entryNodeIds: workflowInteractiveResponse.entryNodeIds,
toolCallId: workflowInteractiveResponseItem?.toolMsgParams.tool_call_id,
memoryMessages: newMessages
}
}
: undefined;
return {
dispatchFlowResponse,
toolNodeInputTokens,
toolNodeOutputTokens,
completeMessages,
assistantResponses: toolNodeAssistants,
toolWorkflowInteractiveResponse,
runTimes,
finish_reason
};
}
return runToolWithToolChoice(
{
...props,
maxRunToolTimes: maxRunToolTimes - 1,
messages: completeMessages
},
{
dispatchFlowResponse,
toolNodeInputTokens,
toolNodeOutputTokens,
assistantResponses: toolNodeAssistants,
runTimes,
finish_reason
}
);
} else {
// No tool is invoked, indicating that the process is over
const gptAssistantResponse: ChatCompletionMessageParam = {
role: ChatCompletionRequestMessageRoleEnum.Assistant,
content: answer,
reasoning_text: reasoningContent
};
const completeMessages = filterMessages.concat(gptAssistantResponse);
inputTokens = inputTokens || (await countGptMessagesTokens(requestMessages, tools));
outputTokens = outputTokens || (await countGptMessagesTokens([gptAssistantResponse]));
// concat tool assistant
const toolNodeAssistant = GPTMessages2Chats([gptAssistantResponse])[0] as AIChatItemType;
return {
dispatchFlowResponse: response?.dispatchFlowResponse || [],
toolNodeInputTokens: response ? response.toolNodeInputTokens + inputTokens : inputTokens,
toolNodeOutputTokens: response ? response.toolNodeOutputTokens + outputTokens : outputTokens,
completeMessages,
assistantResponses: [...assistantResponses, ...toolNodeAssistant.value],
runTimes: (response?.runTimes || 0) + 1,
finish_reason
};
}
};
async function streamResponse({
res,
toolNodes,
stream,
workflowStreamResponse,
aiChatReasoning,
retainDatasetCite
}: {
res: NextApiResponse;
toolNodes: ToolNodeItemType[];
stream: StreamChatType;
workflowStreamResponse?: WorkflowResponseType;
aiChatReasoning: boolean;
retainDatasetCite?: boolean;
}) {
const write = responseWriteController({
res,
readStream: stream
});
let callingTool: { name: string; arguments: string } | null = null;
let toolCalls: ChatCompletionMessageToolCall[] = [];
const { parsePart, getResponseData, updateFinishReason } = parseLLMStreamResponse();
for await (const part of stream) {
if (res.closed) {
stream.controller?.abort();
updateFinishReason('close');
break;
}
const { reasoningContent, responseContent } = parsePart({
part,
parseThinkTag: true,
retainDatasetCite
});
const responseChoice = part.choices?.[0]?.delta;
// Reasoning response
if (aiChatReasoning && reasoningContent) {
workflowStreamResponse?.({
write,
event: SseResponseEventEnum.answer,
data: textAdaptGptResponse({
reasoning_content: reasoningContent
})
});
}
if (responseContent) {
workflowStreamResponse?.({
write,
event: SseResponseEventEnum.answer,
data: textAdaptGptResponse({
text: responseContent
})
});
}
// Parse tool calls
if (responseChoice?.tool_calls?.length) {
responseChoice.tool_calls.forEach((toolCall, i) => {
const index = toolCall.index ?? i;
// Call new tool
const hasNewTool = toolCall?.function?.name || callingTool;
if (hasNewTool) {
// 有 function name,代表新 call 工具
if (toolCall?.function?.name) {
callingTool = {
name: toolCall.function?.name || '',
arguments: toolCall.function?.arguments || ''
};
} else if (callingTool) {
// Continue call(Perhaps the name of the previous function was incomplete)
callingTool.name += toolCall.function?.name || '';
callingTool.arguments += toolCall.function?.arguments || '';
}
if (!callingTool) {
return;
}
const toolNode = toolNodes.find((item) => item.nodeId === callingTool!.name);
if (toolNode) {
// New tool, add to list.
const toolId = getNanoid();
toolCalls[index] = {
...toolCall,
id: toolId,
type: 'function',
function: callingTool,
toolName: toolNode.name,
toolAvatar: toolNode.avatar
};
workflowStreamResponse?.({
event: SseResponseEventEnum.toolCall,
data: {
tool: {
id: toolId,
toolName: toolNode.name,
toolAvatar: toolNode.avatar,
functionName: callingTool.name,
params: callingTool?.arguments ?? '',
response: ''
}
}
});
callingTool = null;
}
} else {
/* arg 追加到当前工具的参数里 */
const arg: string = toolCall?.function?.arguments ?? '';
const currentTool = toolCalls[index];
if (currentTool && arg) {
currentTool.function.arguments += arg;
workflowStreamResponse?.({
write,
event: SseResponseEventEnum.toolParams,
data: {
tool: {
id: currentTool.id,
toolName: '',
toolAvatar: '',
params: arg,
response: ''
}
}
});
}
}
});
}
}
const { reasoningContent, content, finish_reason, usage } = getResponseData();
return {
reasoningContent,
answer: content,
toolCalls: toolCalls.filter(Boolean),
finish_reason,
usage
};
}
import type { ChatItemType } from '@fastgpt/global/core/chat/type';
import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type';
import { dispatchWorkFlow } from '../index';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import type { ModuleDispatchProps, DispatchNodeResultType } from '../../types/runtime';
import { runWorkflow } from '../index';
import { ChatRoleEnum, ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import {
getWorkflowEntryNodeIds,
......@@ -10,15 +10,14 @@ import {
storeNodes2RuntimeNodes,
textAdaptGptResponse
} from '@fastgpt/global/core/workflow/runtime/utils';
import type { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { type NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { filterSystemVariables, getHistories } from '../utils';
import { getHistories } from '../utils';
import { chatValue2RuntimePrompt, runtimePrompt2ChatsValue } from '@fastgpt/global/core/chat/adapt';
import type { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type';
import { authAppByTmbId } from '../../../../support/permission/app/auth';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { getAppVersionById } from '../../../app/version/controller';
import { parseUrlToFileType } from '@fastgpt/global/common/file/tools';
import { parseUrlToFileType } from '../../utils/context';
import type { ChildrenInteractive } from '@fastgpt/global/core/workflow/template/system/interactive/type';
type Props = ModuleDispatchProps<{
......@@ -42,22 +41,17 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
lastInteractive,
node: { pluginId: appId, version },
workflowStreamResponse,
params,
variables
params
} = props;
const {
system_forbid_stream = false,
userChatInput,
history,
fileUrlList,
...childrenAppVariables
} = params;
const { system_forbid_stream = false, userChatInput, history, fileUrlList } = params;
const { files } = chatValue2RuntimePrompt(query);
const userInputFiles = (() => {
if (fileUrlList) {
return fileUrlList.map((url) => parseUrlToFileType(url)).filter(Boolean);
return fileUrlList
.map((url) => parseUrlToFileType(url))
.filter((item): item is NonNullable<typeof item> => !!item);
}
// Adapt version 4.8.13 upgrade
return files;
......@@ -95,15 +89,6 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
const chatHistories = getHistories(history, histories);
// Rewrite children app variables
const systemVariables = filterSystemVariables(variables);
const childrenRunVariables = {
...systemVariables,
...childrenAppVariables,
histories: chatHistories,
appId: String(appData._id)
};
const childrenInteractive =
lastInteractive?.type === 'childrenInteractive'
? lastInteractive.params.childrenResponse
......@@ -121,8 +106,8 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
? query
: runtimePrompt2ChatsValue({ files: userInputFiles, text: userChatInput });
const { flowResponses, flowUsages, assistantResponses, runTimes, workflowInteractiveResponse } =
await dispatchWorkFlow({
const { flowUsages, assistantResponses, runTimes, workflowInteractiveResponse } =
await runWorkflow({
...props,
lastInteractive: childrenInteractive,
// Rewrite stream mode
......@@ -133,7 +118,9 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
}
: {}),
runningAppInfo: {
id: String(appData._id),
sourceType: ChatSourceTypeEnum.app,
sourceId: String(appData._id),
name: appData.name,
teamId: String(appData.teamId),
tmbId: String(appData.tmbId),
isChildApp: true
......@@ -141,7 +128,6 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
runtimeNodes,
runtimeEdges,
histories: chatHistories,
variables: childrenRunVariables,
query: theQuery,
chatConfig
});
......@@ -177,7 +163,6 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
totalPoints: usagePoints,
query: userChatInput,
textOutput: text,
pluginDetail: appData.permission.hasWritePer ? flowResponses : undefined,
mergeSignId: props.node.nodeId
},
[DispatchNodeResponseKeyEnum.nodeDispatchUsages]: [
......@@ -186,8 +171,11 @@ export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
totalPoints: usagePoints
}
],
[DispatchNodeResponseKeyEnum.toolResponses]: text,
answerText: text,
history: completeMessages
[DispatchNodeResponseKeyEnum.toolResponse]: text,
data: {
[NodeOutputKeyEnum.answerText]: text,
[NodeOutputKeyEnum.history]: completeMessages
},
answerText: text
};
};
......@@ -2,7 +2,7 @@ import { MongoApp } from '../../../core/app/schema';
import { AppPermission } from '@fastgpt/global/support/permission/app/controller';
import { Types } from 'mongoose';
import { AppDefaultRoleVal } from '@fastgpt/global/support/permission/app/constant';
import { getResourcePermission } from '../../permission/controller';
import { getTmbPermission } from '../../permission/controller';
import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
/**
......@@ -44,8 +44,8 @@ export async function getUserAppPermission({
};
}
// 3. 使用 getResourcePermission 获取完整权限(包括个人、组、组织权限)
const permissionValue = await getResourcePermission({
// 3. 使用 getTmbPermission 获取完整权限(包括个人、组、组织权限)
const permissionValue = await getTmbPermission({
teamId: String(app.teamId),
tmbId,
resourceId: appId,
......@@ -121,8 +121,8 @@ export async function getUserMultipleAppPermissions({
hasManagePer: permission.hasManagePer
};
} else {
// 使用 getResourcePermission 获取完整权限(包括个人、组、组织权限)
const permissionValue = await getResourcePermission({
// 使用 getTmbPermission 获取完整权限(包括个人、组、组织权限)
const permissionValue = await getTmbPermission({
teamId: String(app.teamId),
tmbId,
resourceId: appId,
......
import type { AuthModeType } from '../type';
import { parseHeaderCert } from '../controller';
import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset';
import { MongoDataset } from '../../../core/dataset/schema';
import { getCollectionWithDataset } from '../../../core/dataset/controller';
import { PermissionTypeEnum } from '@fastgpt/global/support/permission/constant';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import type { AuthResponseType } from '../type';
import type {
CollectionWithDatasetType,
DatasetFileSchema,
DatasetSchemaType
} from '@fastgpt/global/core/dataset/type';
import { getFileById } from '../../../common/file/gridfs/controller';
import { BucketNameEnum } from '@fastgpt/global/common/file/constants';
import { getTmbInfoByTmbId } from '../../user/team/controller';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { MongoDatasetCollection } from '../../../core/dataset/collection/schema';
export async function authDatasetByTmbId({
teamId,
tmbId,
datasetId,
per
}: {
teamId: string;
tmbId: string;
datasetId: string;
per: AuthModeType['per'];
}) {
const { role } = await getTmbInfoByTmbId({ tmbId });
const { dataset, isOwner, canWrite } = await (async () => {
const dataset = await MongoDataset.findOne({ _id: datasetId, teamId }).lean();
if (!dataset) {
return Promise.reject(DatasetErrEnum.unAuthDataset);
}
const isOwner =
String(dataset.tmbId) === tmbId || role === TeamMemberRoleEnum.owner || role === 'admin';
const canWrite = isOwner || dataset.permissionType === PermissionTypeEnum.public;
return { dataset, isOwner, canWrite };
})();
return {
dataset,
isOwner,
canWrite
};
}
export async function authDataset({
datasetId,
per = 'owner',
...props
}: AuthModeType & {
datasetId: string;
}): Promise<
AuthResponseType & {
dataset: DatasetSchemaType;
}
> {
const result = await parseHeaderCert(props);
const { teamId, tmbId } = result;
const { dataset, isOwner, canWrite } = await authDatasetByTmbId({
teamId,
tmbId,
datasetId,
per
});
return {
...result,
dataset,
isOwner,
canWrite
};
}
/*
Read: in team and dataset permission is public
Write: in team, not visitor and dataset permission is public
*/
export async function authDatasetCollection({
collectionId,
per = 'owner',
...props
}: AuthModeType & {
collectionId: string;
}): Promise<
AuthResponseType & {
collection: CollectionWithDatasetType;
}
> {
const { teamId, tmbId } = await parseHeaderCert(props);
const { role } = await getTmbInfoByTmbId({ tmbId });
const { collection, isOwner, canWrite } = await (async () => {
const collection = await getCollectionWithDataset(collectionId);
if (!collection || String(collection.teamId) !== teamId) {
return Promise.reject(DatasetErrEnum.unAuthDatasetCollection);
}
const isOwner =
String(collection.tmbId) === tmbId || role === TeamMemberRoleEnum.owner || role === 'admin';
const canWrite = isOwner;
return {
collection,
isOwner,
canWrite
};
})();
return {
teamId,
tmbId,
collection,
isOwner,
canWrite
};
}
export async function authDatasetFile({
fileId,
per = 'owner',
...props
}: AuthModeType & {
fileId: string;
}): Promise<
AuthResponseType & {
file: DatasetFileSchema;
}
> {
const { teamId, tmbId } = await parseHeaderCert(props);
const [file, collection] = await Promise.all([
getFileById({ bucketName: BucketNameEnum.dataset, fileId }),
MongoDatasetCollection.findOne({
teamId,
fileId
})
]);
if (!file) {
return Promise.reject(CommonErrEnum.fileNotFound);
}
if (!collection) {
return Promise.reject(DatasetErrEnum.unAuthDatasetFile);
}
// file role = collection role
try {
const { isOwner, canWrite } = await authDatasetCollection({
...props,
collectionId: collection._id,
per
});
return {
teamId,
tmbId,
file,
isOwner,
canWrite
};
} catch (error) {
return Promise.reject(DatasetErrEnum.unAuthDatasetFile);
}
}
import { MongoTeamMember } from '../../user/team/teamMemberSchema';
import { GET } from '../../../common/api/plusRequest';
import { checkTeamAIPoints } from '../teamLimit';
import { type UserModelSchema } from '@fastgpt/global/support/user/type';
import { type TeamSchema } from '@fastgpt/global/support/user/team/type';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
type AuthTeamTagTokenProps = {
teamId: string;
teamToken: string;
};
export function authTeamTagToken(data: AuthTeamTagTokenProps) {
return GET<{ uid: string }>('/support/user/team/tag/authTeamToken', data);
}
export async function authTeamSpaceToken({
teamId,
teamToken
}: {
teamId: string;
teamToken: string;
}) {
const [{ uid }, member] = await Promise.all([
authTeamTagToken({ teamId, teamToken }),
MongoTeamMember.findOne({ teamId, role: TeamMemberRoleEnum.owner }, 'tmbId').lean()
]);
return {
uid,
tmbId: member?._id!
};
}
export async function getUserChatInfoAndAuthTeamPoints(tmbId: string) {
const tmb = await MongoTeamMember.findById(tmbId, 'userId teamId')
.populate<{ user: UserModelSchema; team: TeamSchema }>([
......
import type { AuthResponseType } from '@fastgpt/global/support/permission/type';
import type { AuthModeType } from '../type';
import type { TeamItemType } from '@fastgpt/global/support/user/team/type';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import { parseHeaderCert } from '../controller';
import { getTmbInfoByTmbId } from '../../user/team/controller';
import { UserErrEnum } from '../../../../global/common/error/code/user';
export async function authUserNotVisitor(props: AuthModeType): Promise<
AuthResponseType & {
team: TeamItemType;
role: `${TeamMemberRoleEnum}`;
}
> {
const { userId, teamId, tmbId } = await parseHeaderCert(props);
const team: TeamItemType = {
userId: userId,
teamId: teamId,
teamName: '',
memberName: '',
avatar: '',
balance: 0,
tmbId: tmbId,
teamDomain: '',
defaultTeam: true,
role: 'owner',
status: 'active',
canWrite: true,
defaultPermission: 1
};
return {
teamId,
tmbId,
team,
role: team.role,
isOwner: team.role === TeamMemberRoleEnum.owner, // teamOwner
canWrite: true
};
}
/* auth user role */
export async function authUserRole(props: AuthModeType): Promise<
AuthResponseType & {
role: `${TeamMemberRoleEnum}`;
teamOwner: boolean;
}
> {
const result = await parseHeaderCert(props);
// const { role: userRole, canWrite } = await getTmbInfoByTmbId({ tmbId: result.tmbId });
return {
...result,
isOwner: true,
role: TeamMemberRoleEnum.owner,
teamOwner: true,
canWrite: true
};
}
......@@ -41,7 +41,6 @@ export async function getUserDetail({
return Promise.reject(ERROR_ENUM.unAuthorization);
})();
// Validate tmb and userId
if (!tmb || !Types.ObjectId.isValid(tmb.userId)) {
console.log('tmb7777', 'tmb or userId is not valid');
......@@ -74,7 +73,7 @@ export async function getUserDetail({
tmbId: tmb.tmbId,
teamDomain: tmb.teamDomain,
role: tmb.role,
status: tmb.status as 'active' | 'forbidden' | 'leave',
status: tmb.status,
notificationAccount: tmb.notificationAccount,
permission: tmb.permission
},
......
import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
export type PaginationProps<T = Record<string, any>> = T & {
pageSize: number | string;
} & RequireOnlyOne<{
offset: number | string;
pageNum: number | string;
}>;
export type PaginationResponse<T = Record<string, any>> = {
total: number;
list: T[];
};
export type LinkedPaginationProps<T = Record<string, any>> = T & {
pageSize: number;
} & RequireOnlyOne<{
initialId: string;
nextId: string;
prevId: string;
}> &
RequireOnlyOne<{
initialIndex: number;
nextIndex: number;
prevIndex: number;
}>;
export type LinkedListResponse<T = Record<string, any>> = {
list: Array<T & { _id: string; index: number }>;
hasMorePrev: boolean;
hasMoreNext: boolean;
};
......@@ -59,3 +59,5 @@ export const useRequest = <TData, TParams extends any[]>(
return res;
};
export const useRequest2 = useRequest;
This source diff could not be displayed because it is too large. You can view the blob instead.
import type { UserType } from '@fastgpt/global/support/user/type';
import type { PromotionRecordSchema } from '@fastgpt/global/support/activity/type';
export interface ResLogin {
user: UserType;
token: string;
}
export interface PromotionRecordType {
_id: PromotionRecordSchema['_id'];
type: PromotionRecordSchema['type'];
......
......@@ -28,6 +28,7 @@ import {
import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import type { OutLinkEditType, OutLinkSchema } from '@fastgpt/global/support/outLink/type';
import type { OutLinkUpdateBodyType } from '@fastgpt/global/openapi/support/outLink/api';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyBox from '@fastgpt/web/components/common/MyBox';
......@@ -158,7 +159,8 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
icon: 'delete',
type: 'danger',
onClick: () =>
openConfirm(async () => {
openConfirm({
onConfirm: async () => {
setIsLoading(true);
try {
await delShareChatById(item._id);
......@@ -167,6 +169,7 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
console.log(error);
}
setIsLoading(false);
}
})()
}
]
......@@ -256,7 +259,7 @@ function EditLinkModal({
});
const { mutate: onclickUpdate, isLoading: updating } = useRequest({
mutationFn: (e: OutLinkEditType) => {
return putShareChat(e);
return putShareChat(e as unknown as OutLinkUpdateBodyType);
},
errorToast: t('common:common.Update Failed'),
onSuccess: onEdit
......
import React, { useState } from 'react';
import {
Box,
Flex,
Button,
IconButton,
HStack,
ModalBody,
Checkbox,
ModalFooter
} from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { type AppSchema, type AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import { useTranslation } from 'next-i18next';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import TagsEditModal from '../TagsEditModal';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { AppContext } from '@/pageComponents/app/detail/context';
import { useContextSelector } from 'use-context-selector';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { postTransition2Workflow } from '@/web/core/app/api/app';
import { form2AppWorkflow } from '@/web/core/app/utils';
import { type SimpleAppSnapshotType } from './useSnapshots';
import ExportConfigPopover from '@/pageComponents/app/detail/ExportConfigPopover';
import { ChatSidebarPaneEnum } from '@/pageComponents/chat/constants';
const AppCard = ({
appForm,
setPast
}: {
appForm: AppSimpleEditFormType;
setPast: (value: React.SetStateAction<SimpleAppSnapshotType[]>) => void;
}) => {
const router = useRouter();
const { t } = useTranslation();
const onSaveApp = useContextSelector(AppContext, (v) => v.onSaveApp);
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const onOpenInfoEdit = useContextSelector(AppContext, (v) => v.onOpenInfoEdit);
const onDelApp = useContextSelector(AppContext, (v) => v.onDelApp);
const appId = appDetail._id;
const { feConfigs } = useSystemStore();
const [TeamTagsSet, setTeamTagsSet] = useState<AppSchema>();
// transition to workflow
const [transitionCreateNew, setTransitionCreateNew] = useState<boolean>();
const { runAsync: onTransition, loading: transiting } = useRequest2(
async () => {
const { nodes, edges } = form2AppWorkflow(appForm, t);
await onSaveApp({
nodes,
edges,
chatConfig: appForm.chatConfig,
isPublish: false,
versionName: t('app:transition_to_workflow')
});
return postTransition2Workflow({ appId, createNew: transitionCreateNew });
},
{
onSuccess: ({ id }) => {
if (id) {
router.replace({
query: {
appId: id
}
});
} else {
setPast([]);
router.reload();
}
},
successToast: t('common:Success')
}
);
return (
<>
{/* basic info */}
<Box px={[4, 6]} py={4} position={'relative'}>
<Flex alignItems={'center'}>
<Avatar src={appDetail.avatar} borderRadius={'md'} w={'28px'} />
<Box ml={3} fontWeight={'bold'} fontSize={'md'} flex={'1 0 0'} color={'myGray.900'}>
{appDetail.name}
</Box>
</Flex>
<Box
flex={1}
mt={3}
mb={4}
className={'textEllipsis3'}
wordBreak={'break-all'}
color={'myGray.600'}
fontSize={'xs'}
minH={'46px'}
>
应用ID: {appDetail._id}
</Box>
<Box
flex={1}
mt={3}
mb={4}
className={'textEllipsis3'}
wordBreak={'break-all'}
color={'myGray.600'}
fontSize={'xs'}
minH={'46px'}
>
{appDetail.intro || t('common:core.app.tip.Add a intro to app')}
</Box>
<HStack alignItems={'center'}>
<Button
size={['sm', 'md']}
variant={'whitePrimary'}
leftIcon={<MyIcon name={'core/chat/chatLight'} w={'16px'} />}
onClick={() =>
router.push(`/chat?appId=${appId}&pane=${ChatSidebarPaneEnum.RECENTLY_USED_APPS}`)
}
>
{t('common:core.Chat')}
</Button>
{appDetail.permission.hasManagePer && (
<Button
size={['sm', 'md']}
variant={'whitePrimary'}
leftIcon={<MyIcon name={'common/settingLight'} w={'16px'} />}
onClick={onOpenInfoEdit}
>
{t('common:Setting')}
</Button>
)}
{appDetail.permission.isOwner && (
<MyMenu
size={'xs'}
Button={
<IconButton
variant={'whitePrimary'}
size={['smSquare', 'mdSquare']}
icon={<MyIcon name={'more'} w={'1rem'} />}
aria-label={''}
/>
}
menuList={[
{
children: [
{
label: (
<Flex>
<ExportConfigPopover
appName={appDetail.name}
appForm={appForm}
chatConfig={appDetail.chatConfig}
/>
</Flex>
)
},
{
icon: 'core/app/type/workflow',
label: t('app:transition_to_workflow'),
onClick: () => setTransitionCreateNew(true)
},
...(appDetail.permission.hasWritePer && feConfigs?.show_team_chat
? [
{
icon: 'core/chat/fileSelect',
label: t('app:team_tags_set'),
onClick: () => setTeamTagsSet(appDetail)
}
]
: [])
]
},
{
children: [
{
icon: 'delete',
type: 'danger',
label: t('common:Delete'),
onClick: onDelApp
}
]
}
]}
/>
)}
<Box flex={1} />
{/* {isPc && ( */}
{/* <MyTag */}
{/* type="borderFill" */}
{/* colorSchema="gray" */}
{/* onClick={() => (appDetail.permission.hasManagePer ? onOpenInfoEdit() : undefined)} */}
{/* > */}
{/* <PermissionIconText defaultPermission={appDetail.defaultPermission} /> */}
{/* </MyTag> */}
{/* )} */}
</HStack>
</Box>
{TeamTagsSet && <TagsEditModal onClose={() => setTeamTagsSet(undefined)} />}
{transitionCreateNew !== undefined && (
<MyModal isOpen title={t('app:transition_to_workflow')} iconSrc="core/app/type/workflow">
<ModalBody>
<Box mb={3}>{t('app:transition_to_workflow_create_new_tip')}</Box>
<HStack cursor={'pointer'} onClick={() => setTransitionCreateNew((state) => !state)}>
<Checkbox
isChecked={transitionCreateNew}
icon={<MyIcon name={'common/check'} w={'12px'} />}
/>
<Box>{t('app:transition_to_workflow_create_new_placeholder')}</Box>
</HStack>
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} onClick={() => setTransitionCreateNew(undefined)} mr={3}>
{t('common:Close')}
</Button>
<Button variant={'dangerFill'} isLoading={transiting} onClick={() => onTransition()}>
{t('common:Confirm')}
</Button>
</ModalFooter>
</MyModal>
)}
</>
);
};
export default React.memo(AppCard);
......@@ -63,7 +63,6 @@ import {
import { AppToolSourceEnum } from '@fastgpt/global/core/app/tool/constants';
import { getAppPermission } from '@/web/core/app/api';
import { ObjectIdSchema } from '@fastgpt/global/common/type/mongo';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import type { SystemToolVersionType } from '@fastgpt/global/core/app/tool/systemTool/type/base';
import DebugToolTag from '@fastgpt/web/components/core/plugin/tool/DebugToolTag';
import type { WorkflowCheckIssue } from '@fastgpt/global/core/workflow/type/node';
......
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { Box, Flex, HStack } from '@chakra-ui/react';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyBox from '@fastgpt/web/components/common/MyBox';
import React from 'react';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { type NodeTemplateListItemType } from '@fastgpt/global/core/workflow/type/node';
import { type PluginGroupSchemaType } from '@fastgpt/service/core/app/plugin/type';
import UseGuideModal from '@/components/common/Modal/UseGuideModal';
const PluginCard = ({
item,
groups
}: {
item: NodeTemplateListItemType;
groups: PluginGroupSchemaType[];
}) => {
const { t } = useTranslation();
const { feConfigs } = useSystemStore();
const type = groups.reduce<string | undefined>((acc, group) => {
const foundType = group.groupTypes.find((type) => type.typeId === item.templateType);
return foundType ? foundType.typeName : acc;
}, undefined);
return (
<MyBox
key={item.id}
lineHeight={1.5}
h="100%"
pt={4}
pb={3}
px={4}
border={'base'}
boxShadow={'2'}
bg={'white'}
borderRadius={'10px'}
position={'relative'}
display={'flex'}
flexDirection={'column'}
_hover={{
borderColor: 'primary.300',
boxShadow: '1.5'
}}
>
<HStack>
<Avatar src={item.avatar} borderRadius={'sm'} w={'1.5rem'} h={'1.5rem'} />
<Box flex={'1 0 0'} color={'myGray.900'} fontWeight={500}>
{item.name}
</Box>
<Box mr={'-1rem'}>
<Flex
bg={'myGray.100'}
color={'myGray.600'}
py={0.5}
pl={2}
pr={3}
borderLeftRadius={'sm'}
whiteSpace={'nowrap'}
>
<Box ml={1} fontSize={'mini'}>
{t(type as any)}
</Box>
</Flex>
</Box>
</HStack>
<Box
flex={['1 0 48px', '1 0 56px']}
mt={3}
pr={1}
textAlign={'justify'}
wordBreak={'break-all'}
fontSize={'xs'}
color={'myGray.500'}
>
<Box className={'textEllipsis2'}>{item.intro || t('app:templateMarket.no_intro')}</Box>
</Box>
<Flex w={'full'} fontSize={'mini'}>
<Flex flex={1}>
{(item.instructions || item.courseUrl) && (
<UseGuideModal
title={item.name}
iconSrc={item.avatar}
text={item.instructions}
link={item.courseUrl}
>
{({ onClick }) => (
<Flex
color={'primary.700'}
alignItems={'center'}
gap={1}
cursor={'pointer'}
onClick={onClick}
_hover={{ bg: 'myGray.100' }}
>
<MyIcon name={'book'} w={'14px'} />
{t('app:plugin.Instructions')}
</Flex>
)}
</UseGuideModal>
)}
</Flex>
<Box color={'myGray.500'}>{`by ${feConfigs.systemTitle}`}</Box>
</Flex>
</MyBox>
);
};
export default React.memo(PluginCard);
......@@ -10,7 +10,7 @@ import {
type ParentIdType,
type ParentTreePathItemType
} from '@fastgpt/global/common/parentFolder/type';
import { type AppUpdateParams } from '@/global/core/app/api';
import type { UpdateAppBodyType } from '@fastgpt/global/openapi/core/app/common/api';
import dynamic from 'next/dynamic';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { useSystemStore } from '@/web/common/system/useSystemStore';
......@@ -25,7 +25,7 @@ type AppListContextType = {
isFetchingApps: boolean;
folderDetail: AppDetailType | undefined | null;
paths: ParentTreePathItemType[];
onUpdateApp: (id: string, data: AppUpdateParams) => Promise<any>;
onUpdateApp: (id: string, data: UpdateAppBodyType) => Promise<any>;
setMoveAppId: React.Dispatch<React.SetStateAction<string | undefined>>;
refetchFolderDetail: () => Promise<AppDetailType | null>;
searchKey: string;
......@@ -41,7 +41,7 @@ export const AppListContext = createContext<AppListContextType>({
isFetchingApps: false,
folderDetail: undefined,
paths: [],
onUpdateApp: function (id: string, data: AppUpdateParams): Promise<any> {
onUpdateApp: function (id: string, data: UpdateAppBodyType): Promise<any> {
throw new Error('Function not implemented.');
},
setMoveAppId: function (value: React.SetStateAction<string | undefined>): void {
......@@ -74,8 +74,8 @@ const AppListContextProvider = ({ children }: { children: ReactNode }) => {
() => {
const formatType = (() => {
if (!type || type === 'all') return undefined;
if (type === AppTypeEnum.plugin)
return [AppTypeEnum.folder, AppTypeEnum.plugin, AppTypeEnum.httpPlugin];
if (type === AppTypeEnum.workflowTool)
return [AppTypeEnum.folder, AppTypeEnum.workflowTool, AppTypeEnum.httpPlugin];
return [AppTypeEnum.folder, type];
})();
......@@ -108,7 +108,7 @@ const AppListContextProvider = ({ children }: { children: ReactNode }) => {
}
);
const { runAsync: onUpdateApp } = useRequest2((id: string, data: AppUpdateParams) =>
const { runAsync: onUpdateApp } = useRequest2((id: string, data: UpdateAppBodyType) =>
putAppById(id, data).then(async (res) => {
await Promise.all([refetchFolderDetail(), refetchPaths(), loadMyApps()]);
return res;
......@@ -145,6 +145,7 @@ const AppListContextProvider = ({ children }: { children: ReactNode }) => {
// Clear search key when parentId changes
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setSearchKey('');
}, [parentId]);
......
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Box, Flex, Button, Textarea, ModalFooter, HStack, VStack, Image } from '@chakra-ui/react';
import type { UseFormRegister } from 'react-hook-form';
import { useFieldArray, useForm } from 'react-hook-form';
import {
postInsertData2Dataset,
putDatasetDataById,
getDatasetCollectionById,
getDatasetDataItemById
} from '@/web/core/dataset/api';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useTranslation } from 'next-i18next';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getCollectionIcon } from '@fastgpt/global/core/dataset/utils';
import type { DatasetDataIndexItemType } from '@fastgpt/global/core/dataset/type';
import DeleteIcon from '@fastgpt/web/components/common/Icon/delete';
import { defaultCollectionDetail } from '@/web/core/dataset/constants';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import styles from './styles.module.scss';
import {
DatasetDataIndexTypeEnum,
getDatasetIndexMapData
} from '@fastgpt/global/core/dataset/data/constants';
import { DatasetCollectionTypeEnum } from '@fastgpt/global/core/dataset/constants';
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
import MyImage from '@/components/MyImage/index';
export type InputDataType = {
q: string;
a: string;
imagePreivewUrl?: string;
indexes: (Omit<DatasetDataIndexItemType, 'dataId'> & {
dataId?: string; // pg data id
fold: boolean;
})[];
};
enum TabEnum {
chunk = 'chunk',
qa = 'qa',
image = 'image'
}
const InputDataModal = ({
collectionId,
dataId,
defaultValue,
onClose,
onSuccess
}: {
collectionId: string;
dataId?: string;
defaultValue?: { q?: string; a?: string; imagePreivewUrl?: string };
onClose: () => void;
onSuccess: (data: InputDataType & { dataId: string }) => void;
}) => {
const { t } = useTranslation();
const { embeddingModelList, defaultModels } = useSystemStore();
const [currentTab, setCurrentTab] = useState<TabEnum>();
const { register, handleSubmit, reset, control, watch } = useForm<InputDataType>();
const {
fields: indexes,
prepend: prependIndexes,
remove: removeIndexes,
update: updateIndexes
} = useFieldArray({
control,
name: 'indexes'
});
const imagePreivewUrl = watch('imagePreivewUrl');
const { data: collection = defaultCollectionDetail, loading: initLoading } = useRequest2(
async () => {
const [collection, dataItem] = await Promise.all([
getDatasetCollectionById(collectionId),
...(dataId ? [getDatasetDataItemById(dataId)] : [])
]);
if (dataItem) {
setCurrentTab(dataItem?.a ? TabEnum.qa : TabEnum.chunk);
reset({
q: dataItem.q || '',
a: dataItem.a || '',
imagePreivewUrl: dataItem.imagePreivewUrl,
indexes: dataItem.indexes.map((item) => ({
...item,
fold: true
}))
});
} else if (defaultValue) {
setCurrentTab(defaultValue?.a ? TabEnum.qa : TabEnum.chunk);
reset({
q: defaultValue.q || '',
a: defaultValue.a || '',
imagePreivewUrl: defaultValue.imagePreivewUrl
});
} else {
setCurrentTab(TabEnum.chunk);
}
// Forcus reset to image tab
if (collection.type === DatasetCollectionTypeEnum.images) {
setCurrentTab(TabEnum.image);
}
return collection;
},
{
manual: false,
refreshDeps: [collectionId, dataId, defaultValue]
}
);
// Import new data
const { runAsync: sureImportData, loading: isImporting } = useRequest2(
async (e: InputDataType) => {
const data = { ...e };
const postData: any = {
collectionId: collection._id,
q: e.q,
a: currentTab === TabEnum.qa ? e.a : '',
// Contains no default index
indexes: e.indexes?.filter((item) => !!item.text?.trim()) || []
};
const dataId = await postInsertData2Dataset(postData);
return {
...data,
dataId
};
},
{
refreshDeps: [currentTab],
successToast: t('common:dataset.data.Input Success Tip'),
onSuccess(e) {
reset({
...e,
q: '',
a: '',
indexes: []
});
onSuccess(e);
},
errorToast: t('dataset:common.error.unKnow')
}
);
// Update data
const { runAsync: onUpdateData, loading: isUpdating } = useRequest2(
async (e: InputDataType) => {
if (!dataId) return Promise.reject(t('common:error.unKnow'));
const updateData: any = {
dataId,
q: e.q,
a: currentTab === TabEnum.qa ? e.a : '',
indexes: e.indexes.filter((item) => !!item.text?.trim())
};
await putDatasetDataById(updateData);
return {
dataId,
...e
};
},
{
refreshDeps: [currentTab],
successToast: t('common:dataset.data.Update Success Tip'),
onSuccess(data) {
onSuccess(data);
onClose();
}
}
);
const icon = useMemo(
() => getCollectionIcon({ type: collection.type, name: collection.sourceName }),
[collection]
);
const maxToken = useMemo(() => {
const vectorModel =
embeddingModelList.find((item) => item.model === collection.dataset.vectorModel) ||
defaultModels.embedding;
return vectorModel?.maxToken || 2000;
}, [collection.dataset.vectorModel, defaultModels.embedding, embeddingModelList]);
return (
<MyModal
isOpen={true}
isCentered
w={['20rem', '64rem']}
onClose={() => onClose()}
closeOnOverlayClick={false}
maxW={'1440px'}
h={'46.25rem'}
title={
<Flex ml={-3}>
<MyIcon name={icon as any} w={['16px', '20px']} mr={2} />
<Box
className={'textEllipsis'}
wordBreak={'break-all'}
fontSize={'md'}
maxW={['200px', '50vw']}
fontWeight={'500'}
color={'myGray.900'}
whiteSpace={'nowrap'}
overflow={'hidden'}
textOverflow={'ellipsis'}
>
{collection.sourceName || t('common:unknow_source')}
</Box>
</Flex>
}
>
<MyBox
display={'flex'}
flexDir={'column'}
isLoading={initLoading}
h={'100%'}
py={[6, '1.5rem']}
>
{/* Tab */}
<Box px={[5, '3.25rem']}>
{(currentTab === TabEnum.chunk || currentTab === TabEnum.qa) && (
<FillRowTabs
list={[
{ label: t('common:dataset_data_input_chunk'), value: TabEnum.chunk },
{ label: t('common:dataset_data_input_qa'), value: TabEnum.qa }
]}
py={1}
value={currentTab}
onChange={(e) => {
setCurrentTab(e);
}}
/>
)}
</Box>
<Flex flex={'1 0 0'} h={['auto', '0']} gap={6} flexDir={['column', 'row']} px={[5, '0']}>
{/* Data */}
<Flex
pt={4}
pl={[0, '3.25rem']}
flexDir={'column'}
h={'100%'}
gap={3}
flex={'1 0 0'}
w={['100%', 0]}
overflow={['unset', 'auto']}
>
<Flex flexDir={'column'} flex={'1 0 0'} h={0}>
{currentTab === TabEnum.image && (
<>
<FormLabel required mb={1} h={'30px'}>
{t('file:image')}
</FormLabel>
<Box flex={'1 0 0'} h={0} w="100%">
<Box height="100%" position="relative" border="base" borderRadius={'md'} p={1}>
<MyImage
src={imagePreivewUrl}
h="100%"
w="100%"
objectFit="contain"
alt={t('file:Image_Preview')}
/>
</Box>
</Box>
</>
)}
{(currentTab === TabEnum.chunk || currentTab === TabEnum.qa) && (
<>
<FormLabel required mb={1} h={'30px'}>
{currentTab === TabEnum.chunk
? t('common:dataset_data_input_chunk_content')
: t('common:dataset_data_input_q')}
</FormLabel>
<Textarea
resize={'none'}
className={styles.scrollbar}
flex={'1 0 0'}
tabIndex={1}
_focus={{
borderColor: 'primary.500',
boxShadow: '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)',
bg: 'white'
}}
bg={'myGray.25'}
borderRadius={'md'}
borderColor={'myGray.200'}
{...register(`q`, {
required: true
})}
/>
</>
)}
</Flex>
{currentTab === TabEnum.qa && (
<Flex flexDir={'column'} flex={'1 0 0'}>
<FormLabel required mb={1}>
{t('common:dataset_data_input_a')}
</FormLabel>
<Textarea
resize={'none'}
className={styles.scrollbar}
flex={'1 0 0'}
tabIndex={1}
bg={'myGray.25'}
borderRadius={'md'}
border={'1.5px solid '}
borderColor={'myGray.200'}
{...register('a', { required: true })}
/>
</Flex>
)}
{currentTab === TabEnum.image && (
<Flex flexDir={'column'} flex={'1 0 0'}>
<FormLabel required mb={1}>
{t('file:image_description')}
</FormLabel>
<Textarea
resize={'none'}
placeholder={t('file:image_description_tip')}
className={styles.scrollbar}
flex={'1 0 0'}
tabIndex={1}
bg={'myGray.25'}
borderRadius={'md'}
border={'1.5px solid '}
borderColor={'myGray.200'}
{...register('q', {
required: true
})}
/>
</Flex>
)}
</Flex>
{/* Index */}
<Box
pt={4}
pr={[0, '3.25rem']}
flex={'1 0 0'}
w={['100%', 0]}
overflow={['unset', 'auto']}
>
<Flex alignItems={'flex-start'} justifyContent={'space-between'} h={'30px'}>
<FormLabel>
{t('common:dataset.data.edit.Index', {
amount: indexes.length
})}
</FormLabel>
<Button
variant={'whiteBase'}
size={'sm'}
p={0}
transform={'translateY(-6px)'}
onClick={() =>
prependIndexes({
type: DatasetDataIndexTypeEnum.custom,
text: '',
fold: false
})
}
>
<Flex px={'0.62rem'} py={2}>
<MyIcon name={'common/addLight'} w={'1rem'} mr={'0.38rem'} />
{t('common:add_new')}
</Flex>
</Button>
</Flex>
<VStack>
{indexes?.map((index, i) => {
const data = getDatasetIndexMapData(index.type);
return (
<Box
key={index.dataId || i}
p={4}
borderRadius={'md'}
border={'base'}
bg={'myGray.25'}
w={'100%'}
_hover={{
'& .delete': {
display: 'block'
}
}}
>
{/* Header */}
<Flex mb={2} alignItems={'center'}>
<FormLabel flex={'1 0 0'}>{t(data.label)}</FormLabel>
{/* Delete */}
{index.type !== 'default' && (
<HStack className={'delete'} borderRight={'base'} pr={3} mr={2}>
<DeleteIcon
onClick={() => {
removeIndexes(i);
}}
/>
</HStack>
)}
{indexes.length > 1 && (
<MyIconButton
icon={index.fold ? 'core/chat/chevronDown' : 'core/chat/chevronUp'}
onClick={() => {
updateIndexes(i, { ...index, fold: !index.fold });
}}
/>
)}
</Flex>
{/* Content */}
<DataIndexTextArea
disabled={index.type === 'default'}
index={i}
value={index.text}
isFolder={index.fold && indexes.length > 1}
maxToken={maxToken}
register={register}
onFocus={() => {
updateIndexes(i, { ...index, fold: false });
}}
/>
</Box>
);
})}
</VStack>
</Box>
</Flex>
<ModalFooter px={[5, '3.25rem']} py={0} pt={4}>
<MyTooltip label={collection.permission.hasWritePer ? '' : ''}>
<Button
isLoading={isImporting || isUpdating}
// @ts-ignore
onClick={handleSubmit(dataId ? onUpdateData : sureImportData)}
>
{dataId ? t('common:confirm_update') : t('common:comfirm_import')}
</Button>
</MyTooltip>
</ModalFooter>
</MyBox>
</MyModal>
);
};
export default React.memo(InputDataModal);
const textareaMinH = '40px';
const DataIndexTextArea = ({
value,
index,
maxToken,
register,
disabled,
isFolder,
onFocus
}: {
value: string;
index: number;
maxToken: number;
register: UseFormRegister<InputDataType>;
disabled?: boolean;
isFolder: boolean;
onFocus: () => void;
}) => {
const { t } = useTranslation();
const TextareaDom = useRef<HTMLTextAreaElement | null>(null);
const {
ref: TextareaRef,
required,
name,
onChange: onTextChange,
onBlur
} = register(`indexes.${index}.text`, { required: true });
useEffect(() => {
if (TextareaDom.current) {
TextareaDom.current.style.height = textareaMinH;
TextareaDom.current.style.height = `${TextareaDom.current.scrollHeight + 5}px`;
}
}, []);
const autoHeight = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
if (e.target) {
e.target.style.height = textareaMinH;
e.target.style.height = `${e.target.scrollHeight + 5}px`;
}
}, []);
const onclickMark = () => {
TextareaDom?.current?.focus();
onFocus();
};
return (
<Box
pos={'relative'}
{...(isFolder
? {
maxH: '50px',
overflow: 'hidden'
}
: {
maxH: 'auto'
})}
>
{disabled ? (
<Box fontSize={'sm'} color={'myGray.500'} whiteSpace={'pre-wrap'}>
{value}
</Box>
) : (
<Textarea
maxLength={maxToken}
borderColor={'transparent'}
className={styles.scrollbar}
minH={textareaMinH}
px={0}
pt={0}
isRequired={required}
whiteSpace={'pre-wrap'}
resize={'none'}
_focus={{
px: 3,
py: 1,
borderColor: 'primary.500',
boxShadow: '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)',
bg: 'white'
}}
placeholder={t('common:dataset.data.Index Placeholder')}
ref={(e) => {
if (e) TextareaDom.current = e;
TextareaRef(e);
}}
required
name={name}
onChange={(e) => {
autoHeight(e);
onTextChange(e);
}}
onFocus={autoHeight}
onBlur={onBlur}
/>
)}
{isFolder && (
<Box
pos={'absolute'}
bottom={0}
left={0}
right={0}
top={0}
bg={'linear-gradient(182deg, rgba(251, 251, 252, 0.00) 1.76%, #FBFBFC 84.07%)'}
{...(disabled
? {}
: {
cursor: 'pointer',
onClick: onclickMark
})}
/>
)}
</Box>
);
};
......@@ -24,7 +24,6 @@ import {
getCollaboratorList,
postUpdateDatasetCollaborators
} from '@/web/core/dataset/api/collaborator';
import { getModelProvider } from '@fastgpt/global/core/ai/provider';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import MyBox from '@fastgpt/web/components/common/MyBox';
import UserBox from '@fastgpt/web/components/common/UserBox';
......
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { uploadFile } from '@fastgpt/service/common/file/gridfs/controller';
import { getUploadModel } from '@fastgpt/service/common/file/multer';
import { removeFilesByPaths } from '@fastgpt/service/common/file/utils';
import { NextAPI } from '@/service/middleware/entry';
import { createFileToken } from '@fastgpt/service/support/permission/controller';
import { ReadFileBaseUrl } from '@fastgpt/global/common/file/constants';
import { addLog } from '@fastgpt/service/common/system/log';
import { authFrequencyLimit } from '@/service/common/frequencyLimit/api';
import { addSeconds } from 'date-fns';
import { authChatCrud } from '@/service/support/permission/auth/chat';
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
import { type OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
export type UploadChatFileProps = {
appId: string;
} & OutLinkChatAuthProps;
export type UploadDatasetFileProps = {
datasetId: string;
};
const authUploadLimit = (tmbId: string) => {
if (!global.feConfigs.uploadFileMaxAmount) return;
return authFrequencyLimit({
eventId: `${tmbId}-uploadfile`,
maxAmount: global.feConfigs.uploadFileMaxAmount * 2,
expiredTime: addSeconds(new Date(), 30) // 30s
});
};
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const filePaths: string[] = [];
try {
const start = Date.now();
/* Creates the multer uploader */
const upload = getUploadModel({
maxSize: global.feConfigs?.uploadFileMaxSize
});
const { file, bucketName, metadata, data } = await upload.getUploadFile<
UploadChatFileProps | UploadDatasetFileProps
>(req, res);
filePaths.push(file.path);
const { teamId, uid } = await (async () => {
if (bucketName === 'chat') {
const chatData = data as UploadChatFileProps;
const authData = await authChatCrud({
req,
authToken: true,
authApiKey: true,
...chatData
});
return {
teamId: authData.teamId,
uid: authData.uid
};
}
if (bucketName === 'dataset') {
const chatData = data as UploadDatasetFileProps;
const authData = await authDataset({
datasetId: chatData.datasetId,
per: WritePermissionVal,
req,
authToken: true,
authApiKey: true
});
return {
teamId: authData.teamId,
uid: authData.tmbId
};
}
return Promise.reject('bucketName is empty');
})();
//await authUploadLimit(uid);
addLog.info(`Upload file success ${file.originalname}, cost ${Date.now() - start}ms`);
if (!bucketName) {
throw new Error('bucketName is empty');
}
const fileId = await uploadFile({
teamId,
uid,
bucketName,
path: file.path,
filename: file.originalname,
contentType: file.mimetype,
metadata: metadata
});
jsonRes(res, {
data: {
fileId,
previewUrl: `${ReadFileBaseUrl}/${file.originalname}?token=${await createFileToken({
bucketName,
teamId,
uid,
fileId
})}`
}
});
} catch (error) {
jsonRes(res, {
code: 500,
error
});
}
removeFilesByPaths(filePaths);
}
export default NextAPI(handler);
export const config = {
api: {
bodyParser: false
}
};
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { type ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type';
import { countGptMessagesTokens } from '@fastgpt/service/common/string/tiktoken';
export type tokenQuery = {};
export type tokenBody = {
messages: ChatCompletionMessageParam[];
};
export type tokenResponse = {};
async function handler(
req: ApiRequestProps<tokenBody, tokenQuery>,
res: ApiResponseType<any>
): Promise<tokenResponse> {
const start = Date.now();
await authCert({ req, authRoot: true });
const tokens = await countGptMessagesTokens(req.body.messages);
return {
tokens,
time: Date.now() - start,
memory: process.memoryUsage()
};
}
export default NextAPI(handler);
export const config = {
api: {
bodyParser: {
sizeLimit: '200mb'
},
responseLimit: '200mb'
}
};
import type { NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import type { GetChatSpeechProps } from '@/global/core/chat/api';
import { text2Speech } from '@fastgpt/service/core/ai/audio/speech';
import { pushAudioSpeechUsage } from '@/service/support/wallet/usage/push';
import { authChatCrud } from '@/service/support/permission/auth/chat';
import { authType2UsageSource } from '@/service/support/wallet/usage/utils';
import { getTTSModel } from '@fastgpt/service/core/ai/model';
import { MongoTTSBuffer } from '@fastgpt/service/common/buffer/tts/schema';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
/*
1. get tts from chatItem store
2. get tts from ai
4. push bill
*/
async function handler(req: ApiRequestProps<GetChatSpeechProps>, res: NextApiResponse) {
try {
const { ttsConfig, input } = req.body;
if (!ttsConfig.model || !ttsConfig.voice) {
throw new Error('model or voice not found');
}
const { teamId, tmbId, authType } = await authChatCrud({
req,
authToken: true,
authApiKey: true,
...req.body
});
// 获取团队的openaiAccount信息
const team = await MongoTeam.findById(teamId, 'openaiAccount').lean();
const userKey = team?.openaiAccount;
const ttsModel = getTTSModel(ttsConfig.model);
const voiceData = ttsModel.voices?.find((item) => item.value === ttsConfig.voice);
if (!voiceData) {
throw new Error('voice not found');
}
const bufferId = `${ttsModel.model}-${ttsConfig.voice}`;
/* get audio from buffer */
const ttsBuffer = await MongoTTSBuffer.findOne(
{
bufferId,
text: JSON.stringify({ text: input, speed: ttsConfig.speed })
},
'buffer'
);
if (ttsBuffer?.buffer) {
return res.end(new Uint8Array(ttsBuffer.buffer.buffer));
}
/* request audio */
await text2Speech({
res,
input,
model: ttsConfig.model,
voice: ttsConfig.voice,
speed: ttsConfig.speed,
userKey,
onSuccess: async ({ model, buffer }) => {
try {
/* bill */
pushAudioSpeechUsage({
model: model,
charsLength: input.length,
tmbId,
teamId,
source: authType2UsageSource({ authType })
});
/* create buffer */
await MongoTTSBuffer.create(
{
bufferId,
text: JSON.stringify({ text: input, speed: ttsConfig.speed }),
buffer
},
ttsModel.requestUrl && ttsModel.requestAuth
? {
path: ttsModel.requestUrl,
headers: {
Authorization: `Bearer ${ttsModel.requestAuth}`
}
}
: {}
);
} catch (error) {}
},
onError: (err) => {
jsonRes(res, {
code: 500,
error: err
});
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
// 不能使用 NextApiResponse
export default handler;
import { NextAPI } from '@/service/middleware/entry';
import { authChatCrud, authCollectionInChat } from '@/service/support/permission/auth/chat';
import {
type DatasetCiteItemType,
type DatasetDataSchemaType
} from '@fastgpt/global/core/dataset/type';
import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema';
import { type ApiRequestProps } from '@fastgpt/service/type/next';
import {
type LinkedListResponse,
type LinkedPaginationProps
} from '@fastgpt/web/common/fetch/type';
import { type FilterQuery, Types } from 'mongoose';
import { quoteDataFieldSelector } from '@/service/core/chat/constants';
import { processChatTimeFilter } from '@/service/core/chat/utils';
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { getCollectionWithDataset } from '@fastgpt/service/core/dataset/controller';
import { getFormatDatasetCiteList } from '@fastgpt/service/core/dataset/data/controller';
export type GetCollectionQuoteProps = LinkedPaginationProps & {
chatId: string;
chatItemDataId: string;
collectionId: string;
appId: string;
shareId?: string;
outLinkUid?: string;
teamId?: string;
teamToken?: string;
};
export type GetCollectionQuoteRes = LinkedListResponse<DatasetCiteItemType>;
type BaseMatchType = FilterQuery<DatasetDataSchemaType>;
async function handler(
req: ApiRequestProps<GetCollectionQuoteProps>
): Promise<GetCollectionQuoteRes> {
const {
initialId,
initialIndex,
prevId,
prevIndex,
nextId,
nextIndex,
collectionId,
chatItemDataId,
appId,
chatId,
shareId,
outLinkUid,
teamId,
teamToken,
pageSize = 15
} = req.body;
const limitedPageSize = Math.min(pageSize, 30);
const [collection, { chat, showRawSource }, { chatItem }] = await Promise.all([
getCollectionWithDataset(collectionId),
authChatCrud({
req,
authToken: true,
appId,
chatId,
shareId,
outLinkUid,
teamId,
teamToken
}),
authCollectionInChat({ appId, chatId, chatItemDataId, collectionIds: [collectionId] })
]);
if (!showRawSource) {
return Promise.reject(ChatErrEnum.unAuthChat);
}
if (!chat) return Promise.reject(ChatErrEnum.unAuthChat);
const baseMatch: BaseMatchType = {
teamId: collection.teamId,
datasetId: collection.datasetId,
collectionId,
$or: [
{ updateTime: { $lt: new Date(chatItem.time) } },
{ history: { $elemMatch: { updateTime: { $lt: new Date(chatItem.time) } } } }
]
};
if (initialId && initialIndex !== undefined) {
return await handleInitialLoad({
initialId,
initialIndex,
pageSize: limitedPageSize,
chatTime: chatItem.time,
baseMatch
});
}
if ((prevId && prevIndex !== undefined) || (nextId && nextIndex !== undefined)) {
return await handlePaginatedLoad({
prevId,
prevIndex,
nextId,
nextIndex,
pageSize: limitedPageSize,
chatTime: chatItem.time,
baseMatch
});
}
return { list: [], hasMorePrev: false, hasMoreNext: false };
}
export default NextAPI(handler);
async function handleInitialLoad({
initialId,
initialIndex,
pageSize,
chatTime,
baseMatch
}: {
initialId: string;
initialIndex: number;
pageSize: number;
chatTime: Date;
baseMatch: BaseMatchType;
}): Promise<GetCollectionQuoteRes> {
const centerNode = await MongoDatasetData.findOne(
{
_id: new Types.ObjectId(initialId)
},
quoteDataFieldSelector
).lean();
if (!centerNode) {
const list = await MongoDatasetData.find(baseMatch, quoteDataFieldSelector)
.sort({ chunkIndex: 1, _id: 1 })
.limit(pageSize)
.lean();
const hasMoreNext = list.length === pageSize;
return {
list: processChatTimeFilter(getFormatDatasetCiteList(list), chatTime),
hasMorePrev: false,
hasMoreNext
};
}
const prevHalfSize = Math.floor(pageSize / 2);
const nextHalfSize = pageSize - prevHalfSize - 1;
const { list: prevList, hasMore: hasMorePrev } = await getPrevNodes(
initialId,
initialIndex,
prevHalfSize,
baseMatch
);
const { list: nextList, hasMore: hasMoreNext } = await getNextNodes(
initialId,
initialIndex,
nextHalfSize,
baseMatch
);
const resultList = [...prevList, centerNode, ...nextList];
return {
list: processChatTimeFilter(getFormatDatasetCiteList(resultList), chatTime),
hasMorePrev,
hasMoreNext
};
}
async function handlePaginatedLoad({
prevId,
prevIndex,
nextId,
nextIndex,
pageSize,
chatTime,
baseMatch
}: {
prevId: string | undefined;
prevIndex: number | undefined;
nextId: string | undefined;
nextIndex: number | undefined;
pageSize: number;
chatTime: Date;
baseMatch: BaseMatchType;
}): Promise<GetCollectionQuoteRes> {
const { list, hasMore } =
prevId && prevIndex !== undefined
? await getPrevNodes(prevId, prevIndex, pageSize, baseMatch)
: await getNextNodes(nextId!, nextIndex!, pageSize, baseMatch);
const processedList = processChatTimeFilter(getFormatDatasetCiteList(list), chatTime);
return {
list: processedList,
hasMorePrev: !!prevId && hasMore,
hasMoreNext: !!nextId && hasMore
};
}
async function getPrevNodes(
initialId: string,
initialIndex: number,
limit: number,
baseMatch: BaseMatchType
): Promise<{
list: DatasetDataSchemaType[];
hasMore: boolean;
}> {
const match: BaseMatchType = {
...baseMatch,
$or: [
{ chunkIndex: { $lt: initialIndex } },
{ chunkIndex: initialIndex, _id: { $lt: new Types.ObjectId(initialId) } }
]
};
const list = await MongoDatasetData.find(match, quoteDataFieldSelector)
.sort({ chunkIndex: -1, _id: -1 })
.limit(limit)
.lean();
return {
list: list.filter((item) => String(item._id) !== initialId).reverse(),
hasMore: list.length === limit
};
}
async function getNextNodes(
initialId: string,
initialIndex: number,
limit: number,
baseMatch: BaseMatchType
): Promise<{
list: DatasetDataSchemaType[];
hasMore: boolean;
}> {
const match: BaseMatchType = {
...baseMatch,
$or: [
{ chunkIndex: { $gt: initialIndex } },
{ chunkIndex: initialIndex, _id: { $gt: new Types.ObjectId(initialId) } }
]
};
const list = await MongoDatasetData.find(match, quoteDataFieldSelector)
.sort({ chunkIndex: 1, _id: 1 })
.limit(limit)
.lean();
return {
list: list.filter((item) => String(item._id) !== initialId),
hasMore: list.length === limit
};
}
import type { InitChatResponse, InitTeamChatProps } from '@/global/core/chat/api';
import { getChatModelNameListByModules } from '@/service/core/app/workflow';
import { NextAPI } from '@/service/middleware/entry';
import { type ApiRequestProps } from '@fastgpt/service/type/next';
import { authTeamSpaceToken } from '@/service/support/permission/auth/team';
import { type ApiRequestProps } from '@fastgpt/next/type';
import { authTeamSpaceToken } from '@fastgpt/service/support/permission/auth/team';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
......@@ -15,7 +15,7 @@ import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
import type { NextApiResponse } from 'next';
async function handler(req: ApiRequestProps<InitTeamChatProps>, res: NextApiResponse) {
let { teamId, appId, chatId, teamToken } = req.query;
const { teamId, appId, chatId, teamToken } = req.query;
if (!teamId || !appId || !teamToken) {
return Promise.reject('teamId, appId, teamToken are required');
......
......@@ -26,7 +26,6 @@ async function handler(req: ApiRequestProps) {
});
const deleteDatasets = await findDatasetAndAllChildren({
teamId,
datasetId,
fields: '_id'
});
......
......@@ -35,12 +35,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { datasetId } = ExportDatasetQuerySchema.parse(req.query);
// 凭证校验
// const { teamId } = await authDataset({
// req,
// authToken: true,
// datasetId,
// per: WritePermissionVal
// });
const { teamId } = await authDataset({
req,
authToken: true,
datasetId,
per: WritePermissionVal
});
// await checkExportDatasetLimit({
// teamId,
......
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import type { ApiRequestProps } from '@fastgpt/next/type';
import { MongoChatFeedbackLog } from '@fastgpt/service/core/chat/chatfeedbacklogSchema';
import { MongoChatFeedbackLogRelation } from '@fastgpt/service/core/chat/chatfeedbacklogrelationSchema';
import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema';
import { MongoDataset } from '@fastgpt/service/core/dataset/schema';
import { createOneCollection } from '@fastgpt/service/core/dataset/collection/controller';
import { ChatFeedbackStatusEnum } from '@fastgpt/global/core/chat/constants';
import { insertData2Dataset } from '@/service/core/dataset/data/controller';
import { createDatasetData } from '@/service/core/dataset/data/data';
import { simpleText } from '@fastgpt/global/common/string/tools';
import { connectionMongo } from '@fastgpt/service/common/mongo';
import { DatasetCollectionTypeEnum } from '@fastgpt/global/core/dataset/constants';
......@@ -41,7 +41,7 @@ async function getDataset(datasetId: string) {
// 获取或创建数据集集合
async function getOrCreateCollection(datasetId: string, feedbackLog: any) {
const relation = await MongoChatFeedbackLogRelation.findOne({ datasetId }).lean();
let collection = relation
const collection = relation
? await MongoDatasetCollection.findById(relation.collectionId).lean()
: null;
let collectionId = collection?._id || '';
......@@ -79,7 +79,7 @@ async function handler(req: ApiRequestProps<ApproveBody>) {
const formatQ = simpleText(q);
const formatA = simpleText(a);
try {
await insertData2Dataset({
await createDatasetData({
teamId: feedbackLog.teamId,
tmbId: feedbackLog.tmbId,
datasetId,
......
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import type { ApiRequestProps } from '@fastgpt/next/type';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { MongoChatFeedbackLog } from '@fastgpt/service/core/chat/chatfeedbacklogSchema';
import { MongoChatFeedbackLogRelation } from '@fastgpt/service/core/chat/chatfeedbacklogrelationSchema';
......
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import type { ApiRequestProps } from '@fastgpt/next/type';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { Types } from 'mongoose';
......
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import type { ApiRequestProps } from '@fastgpt/next/type';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
import { Types } from 'mongoose';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { parseHeaderCert } from '@fastgpt/service/support/permission/controller';
import { parseHeaderCert } from '@fastgpt/service/support/permission/auth/common';
export type CollaboratorQuery = {
tmbId: string;
appId: string;
......
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import type { ApiRequestProps } from '@fastgpt/next/type';
import { Types } from 'mongoose';
import type { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator';
import type { CollaboratorItemDetailType } from '@fastgpt/global/support/permission/collaborator';
import { TeamMemberCollectionName } from '@fastgpt/global/support/user/team/constant';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
import { Permission } from '@fastgpt/global/support/permission/controller';
......@@ -44,7 +44,7 @@ async function handler(req: ApiRequestProps<any, CollaboratorBody>) {
}
])) as CollaboratorType[];
const list: CollaboratorItemType[] = collaborators.map((item) => ({
const list: CollaboratorItemDetailType[] = collaborators.map((item) => ({
...item,
permission: new Permission({ role: item.permission })
}));
......
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import type { ApiRequestProps } from '@fastgpt/next/type';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
import type { UpdateAppCollaboratorBody } from '@fastgpt/global/core/app/collaborator';
import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
import { Types } from 'mongoose';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { parseHeaderCert } from '@fastgpt/service/support/permission/controller';
import { parseHeaderCert } from '@fastgpt/service/support/permission/auth/common';
async function handler(req: ApiRequestProps<UpdateAppCollaboratorBody>) {
const { appId, groups, members, orgs, permission } = req.body;
......
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import type { ApiRequestProps } from '@fastgpt/next/type';
import { getUserAppPermission } from '@fastgpt/service/support/permission/app/getUserAppPermission';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
......
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import type { ApiRequestProps } from '@fastgpt/next/type';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
import { Types } from 'mongoose';
import { MongoDataset } from '@fastgpt/service/core/dataset/schema';
import type { DatasetCollaboratorDeleteParams } from '@fastgpt/global/core/dataset/collaborator';
import { parseHeaderCert } from '@fastgpt/service/support/permission/controller';
import { parseHeaderCert } from '@fastgpt/service/support/permission/auth/common';
async function handler(req: ApiRequestProps<any, DatasetCollaboratorDeleteParams>) {
const { tmbId, datasetId } = req.query;
......
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import type { ApiRequestProps } from '@fastgpt/next/type';
import { Types } from 'mongoose';
import type { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator';
import type { CollaboratorItemDetailType } from '@fastgpt/global/support/permission/collaborator';
import { TeamMemberCollectionName } from '@fastgpt/global/support/user/team/constant';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
import { Permission } from '@fastgpt/global/support/permission/controller';
......@@ -44,7 +44,7 @@ async function handler(req: ApiRequestProps<any, CollaboratorQuery>) {
}
])) as CollaboratorType[];
const list: CollaboratorItemType[] = collaborators.map((item) => ({
const list: CollaboratorItemDetailType[] = collaborators.map((item) => ({
...item,
permission: new Permission({ role: item.permission })
}));
......
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import type { ApiRequestProps } from '@fastgpt/next/type';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
import { Types } from 'mongoose';
import { MongoDataset } from '@fastgpt/service/core/dataset/schema';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import type { UpdateDatasetCollaboratorBody } from '@fastgpt/global/core/dataset/collaborator';
import { parseHeaderCert } from '@fastgpt/service/support/permission/controller';
import { parseHeaderCert } from '@fastgpt/service/support/permission/auth/common';
async function handler(req: ApiRequestProps<UpdateDatasetCollaboratorBody>) {
const { datasetId, groups, members, orgs, permission } = req.body;
......
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import type { ApiRequestProps } from '@fastgpt/next/type';
import { getUserDatasetCollaboratorPermission } from '@fastgpt/service/support/permission/dataset/getDatasetPermissions';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { MongoDataset } from '@fastgpt/service/core/dataset/schema';
......
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import type { ApiRequestProps } from '@fastgpt/next/type';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
......
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import type { ApiRequestProps } from '@fastgpt/next/type';
export type GroupBody = {
searchKey?: string;
};
......
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import type { ApiRequestProps } from '@fastgpt/next/type';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import type { TeamMemberSchema } from '@fastgpt/global/support/user/team/type';
import type { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
......@@ -60,7 +60,7 @@ async function handler(
avatar: item.avatar,
createTime: item.createTime,
memberName: item.name,
orgs: [] as String[],
orgs: [] as string[],
role: item.role,
status: item.status,
teamId: item.teamId,
......
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import type { ApiRequestProps } from '@fastgpt/next/type';
export type OrgBody = {
searchKey?: string;
};
......
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useRouter } from 'next/router';
import { Box, Flex, IconButton } from '@chakra-ui/react';
import { streamFetch } from '@/web/common/api/fetch';
import SideBar from '@/components/SideBar';
import ChatBox from '@/components/core/chat/ChatContainer/ChatBox';
import type { StartChatFnProps } from '@/components/core/chat/ChatContainer/type';
import PageContainer from '@/components/PageContainer';
import { serviceSideProps } from '@/web/common/i18n/utils';
import { LANG_KEY, SHARE_LANG_KEY } from '@fastgpt/web/i18n/utils';
import { useTranslation } from 'next-i18next';
import { getInitOutLinkChatInfo } from '@/web/core/chat/api';
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import { getLogger, LogCategories } from '@fastgpt/service/common/logger';
import NextHead from '@/components/common/NextHead';
import { useContextSelector } from 'use-context-selector';
import ChatContextProvider, { ChatContext } from '@/web/core/chat/context/chatContext';
import { ChatSourceTypeEnum, GetChatTypeEnum } from '@fastgpt/global/core/chat/constants';
import { useMount } from 'ahooks';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import dynamic from 'next/dynamic';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useShareChatStore } from '@/web/core/chat/storeShareChat';
import ChatItemContextProvider, { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import ChatRecordContextProvider, {
ChatRecordContext
} from '@/web/core/chat/context/chatRecordContext';
import { getDisplayHistoryTitle } from '@/web/core/chat/context/historyTitleUtils';
import { useChatStore } from '@/web/core/chat/context/useChatStore';
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
import { type AppSchemaType } from '@fastgpt/global/core/app/type';
import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { ChatTypeEnum } from '@/components/core/chat/ChatContainer/ChatBox/constants';
import ChatHistorySidebar from '@/pageComponents/chat/slider/ChatSliderSidebar';
import ChatSliderMobileDrawer from '@/pageComponents/chat/slider/ChatSliderMobileDrawer';
import { useMemoEnhance } from '@fastgpt/web/hooks/useMemoEnhance';
import ChatLanguageSelector from '@/pageComponents/chat/LanguageSelector';
import ChatWindowHeader from '@/pageComponents/chat/ChatWindow/ChatWindowHeader';
import MyIcon from '@fastgpt/web/components/common/Icon';
import ToolMenu from '@/pageComponents/chat/ToolMenu';
import { mobileChatHeaderIconButtonStyle } from '@/pageComponents/chat/ChatWindow/headerIconButtonStyle';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { getAppChatSourceKey } from '@/web/core/chat/utils';
import { useAppChatGenerateStatusSync } from '@/pageComponents/chat/ChatWindow/useAppChatGenerateStatusSync';
import { postMarkChatRead } from '@/web/core/chat/history/api';
import { useSandboxEditor, useSandboxStatus } from '@/pageComponents/chat/SandboxEditor/hook';
import type { GetHistoriesBodyType } from '@fastgpt/global/openapi/core/chat/history/api';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import Watermark from '@/components/common/Watermark';
const logger = getLogger(LogCategories.MODULE.CHAT.ITEM);
const CustomPluginRunBox = dynamic(() => import('@/pageComponents/chat/CustomPluginRunBox'));
type Props = {
appId: string;
appName: string;
appIntro: string;
appAvatar: string;
shareId: string;
authToken: string;
customUid: string;
canDownloadSource: boolean;
isShowCite: boolean;
isShowFullText: boolean;
showRunningStatus: boolean;
showSkillReferences: boolean;
};
const OutLink = (props: Props) => {
const { t } = useTranslation();
const router = useRouter();
const {
shareId = '',
watermark = '',
showHistory = '1',
showHead = '1',
authToken,
customUid,
showWorkorder,
hideMenu = '0',
...customVariables
} = router.query as {
shareId: string;
watermark: string;
showHistory: '0' | '1';
showHead: '0' | '1';
authToken: string;
showWorkorder: '0' | '1';
hideMenu: '0' | '1';
[key: string]: string;
};
const { isPc } = useSystem();
let { outLinkAuthData, appId, chatId } = useChatStore();
if (props.customUid) {
chatId = props.customUid + '_' + chatId;
}
// Remove empty value field
const formatedCustomVariables = useMemo(() => {
return Object.fromEntries(Object.entries(customVariables).filter(([_, value]) => value !== ''));
}, [customVariables]);
const forbidLoadChatRef = useContextSelector(ChatContext, (v) => v.forbidLoadChat);
const onChangeChatId = useContextSelector(ChatContext, (v) => v.onChangeChatId);
const onOpenSlider = useContextSelector(ChatContext, (v) => v.onOpenSlider);
const onCloseSlider = useContextSelector(ChatContext, (v) => v.onCloseSlider);
const resetVariables = useContextSelector(ChatItemContext, (v) => v.resetVariables);
const clearChatRecords = useContextSelector(ChatItemContext, (v) => v.clearChatRecords);
const isPlugin = useContextSelector(ChatItemContext, (v) => v.isPlugin);
const chatBoxData = useContextSelector(ChatItemContext, (v) => v.chatBoxData);
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData);
const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData);
const isShowCite = useContextSelector(ChatItemContext, (v) => v.isShowCite);
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const isChatRecordsLoaded = useContextSelector(ChatRecordContext, (v) => v.isChatRecordsLoaded);
const onChatGenerateStatusChange = useAppChatGenerateStatusSync();
const currentHistory = useContextSelector(ChatContext, (v) =>
v.histories.find((item) => item.chatId === chatId && item.appId === appId)
);
const chatWindowTitle = getDisplayHistoryTitle({
customTitle: currentHistory?.customTitle,
title: chatBoxData.title,
fallbackTitle: t('common:core.chat.New Chat')
});
const initSign = useRef(false);
const { data, loading } = useRequest(
async () => {
const shareId = outLinkAuthData.shareId;
const outLinkUid = outLinkAuthData.outLinkUid;
if (!outLinkUid || !shareId || forbidLoadChatRef.current) return;
const res = await getInitOutLinkChatInfo({
chatId,
outLinkAuthData: {
shareId,
outLinkUid
}
});
const sharePassword = res.sharePassword;
if (sharePassword) {
const password = localStorage.getItem('shareAuthCode_' + shareId);
if (!password || password !== sharePassword) {
// 密码错误, 跳转到密码输入页面
router.push('/login/sharelogin?shareId=' + shareId);
}
}
setChatBoxData({
...res,
appId,
sourceKey: getAppChatSourceKey(appId)
});
resetVariables({
variables: {
...formatedCustomVariables,
...res.variables
},
variableList: res.app?.chatConfig?.variables
});
return res;
},
{
manual: false,
refreshDeps: [shareId, outLinkAuthData, chatId],
onFinally() {
forbidLoadChatRef.current = false;
}
}
);
const mobileHeaderAppName = props.appName || data?.app?.name || chatBoxData.app.name;
const mobileHeaderAppAvatar = props.appAvatar || data?.app?.avatar || chatBoxData.app.avatar;
const isShareAuthReady = !!outLinkAuthData.shareId && !!outLinkAuthData.outLinkUid;
const { SandboxEntryIcon } = useSandboxStatus({
appId: isShareAuthReady ? appId : '',
chatId,
outLinkAuthData,
enabled: isShareAuthReady
});
const { SandboxEditorModal, onOpenSandboxModal } = useSandboxEditor({
appId,
chatId,
outLinkAuthData,
enabled: isShareAuthReady
});
useEffect(() => {
if (initSign.current === false && data && isChatRecordsLoaded) {
initSign.current = true;
if (window !== top) {
window.top?.postMessage({ type: 'shareChatReady' }, '*');
}
}
}, [data, isChatRecordsLoaded]);
const startChat = useCallback(
async ({
messages,
controller,
generatingMessage,
variables,
responseChatItemId
}: StartChatFnProps) => {
let id = getNanoid();
if (props.customUid) {
id = props.customUid + '_' + id;
}
const completionChatId = chatId || id;
const histories = messages.slice(-1);
//post message to report chat start
window.top?.postMessage(
{
type: 'shareChatStart',
data: {
question: histories[0]?.content
}
},
'*'
);
const { responseText } = await streamFetch({
data: {
messages: histories,
variables: {
...variables,
...customVariables
},
responseChatItemId,
chatId: completionChatId,
outLinkAuthData,
retainDatasetCite: isShowCite,
showSkillReferences: props.showSkillReferences
},
onMessage: generatingMessage,
abortCtrl: controller
});
// new chat
if (completionChatId !== chatId) {
onChangeChatId(completionChatId, true);
}
// hook message
window.top?.postMessage(
{
type: 'shareChatFinish',
data: {
question: histories[0]?.content,
answer: responseText
}
},
'*'
);
return { responseText, isNewChat: forbidLoadChatRef.current };
},
[
chatId,
customVariables,
outLinkAuthData,
isShowCite,
props.showSkillReferences,
forbidLoadChatRef,
onChangeChatId
]
);
// window init
const [isEmbed, setIdEmbed] = useState(true);
useMount(() => {
setIdEmbed(window !== top);
});
const RenderHistoryList = useMemo(() => {
// 语言入口跟随历史侧栏挂载:PC 放侧栏底部,移动端放抽屉底部且选择后关闭抽屉。
const footerSlot = (
<Box flexShrink={0} p={3} mt="auto">
<ChatLanguageSelector mode="share" onSelected={isPc ? undefined : onCloseSlider} />
</Box>
);
const Children = (
<ChatHistorySidebar
menuConfirmButtonText={t('chat:confirm_to_clear_share_chat_history')}
footerSlot={footerSlot}
/>
);
if (showHistory !== '1') return null;
return isPc ? (
<SideBar externalTrigger={!!datasetCiteData}>{Children}</SideBar>
) : (
<ChatSliderMobileDrawer
showHeader={false}
showFooter={false}
footerSlot={footerSlot}
menuConfirmButtonText={t('common:core.chat.Confirm to clear history')}
/>
);
}, [isPc, datasetCiteData, onCloseSlider, showHistory, t]);
return (
<>
<Watermark content={watermark}>
<NextHead
title={props.appName || data?.app?.name || 'AI'}
desc={props.appIntro || data?.app?.intro}
icon={props.appAvatar || data?.app?.avatar}
/>
<Flex
h={'full'}
minH={0}
minW={0}
gap={datasetCiteData ? 0 : 4}
{...(isEmbed ? { p: '0 !important', borderRadius: '0', boxShadow: 'none' } : { p: [0, 5] })}
>
{(!datasetCiteData || isPc) && (
<PageContainer
flex={'1 0 0'}
w={0}
minH={0}
minW={0}
p={'0 !important'}
insertProps={
datasetCiteData
? {
borderRadius: [0, '16px 0 0 16px']
}
: undefined
}
>
<Flex h={'100%'} minH={0} minW={0} flexDirection={['column', 'row']}>
{RenderHistoryList}
{/* chat container */}
<Flex
position={'relative'}
h={[0, '100%']}
minH={0}
minW={0}
w={['100%', 0]}
flex={'1 0 0'}
flexDirection={'column'}
>
{/* header */}
{showHead === '1' &&
!isPlugin &&
(isPc ? (
<ChatWindowHeader
title={chatWindowTitle}
history={chatRecords}
chatType={ChatTypeEnum.chat}
rightActions={<SandboxEntryIcon onOpen={onOpenSandboxModal} />}
/>
) : (
<Flex
h="48px"
px={4}
bg="white"
alignItems="center"
justifyContent="space-between"
color="myGray.600"
>
{showHistory === '1' ? (
<IconButton
aria-label="Open history"
icon={
<MyIcon
name="core/chat/sidebar/menu"
w="20px"
h="20px"
color="currentColor"
/>
}
variant="unstyled"
{...mobileChatHeaderIconButtonStyle}
onClick={onOpenSlider}
/>
) : (
<Box minW="36px" />
)}
<Flex
alignItems="center"
minW={0}
flex="1"
justifyContent="center"
px={3}
gap={2}
>
{!!mobileHeaderAppAvatar && (
<Avatar
src={mobileHeaderAppAvatar}
w="24px"
h="24px"
borderRadius="6px"
flexShrink={0}
/>
)}
<MyTooltip label={mobileHeaderAppName} showOnlyWhenOverflow>
<Box
minW={0}
fontSize="16px"
fontWeight={500}
color="myGray.900"
overflow="hidden"
whiteSpace="nowrap"
textOverflow="clip"
>
{mobileHeaderAppName}
</Box>
</MyTooltip>
</Flex>
{hideMenu === '1' ? (
<Box minW="36px" />
) : (
<Box minW="36px">
<ToolMenu
history={chatRecords}
reserveSpace={showWorkorder !== undefined}
chatType={ChatTypeEnum.share}
/>
</Box>
)}
</Flex>
))}
{/* chat box */}
<Box flex={1} minH={0} minW={0} overflow={'hidden'} bg={'white'}>
{isPlugin ? (
<CustomPluginRunBox
appId={appId}
chatId={chatId}
outLinkAuthData={outLinkAuthData}
onNewChat={() => {
clearChatRecords();
onChangeChatId(getNanoid());
}}
onStartChat={startChat}
/>
) : (
<ChatBox
isReady={!loading}
sourceTarget={{ sourceType: ChatSourceTypeEnum.app, sourceId: appId }}
chatId={chatId}
outLinkAuthData={outLinkAuthData}
features={{
autoResume: true,
feedbackType: 'user',
workorder: showWorkorder === '1',
quickReplies: true,
inputGuide: true,
voice: true,
tts: true,
sandbox: true
}}
onStartChat={startChat}
onMarkChatRead={postMarkChatRead}
onChatGenerateStatusChange={onChatGenerateStatusChange}
chatType={ChatTypeEnum.share}
/>
)}
</Box>
<SandboxEditorModal />
</Flex>
</Flex>
</PageContainer>
)}
{datasetCiteData && (
<PageContainer
flex={'1 0 0'}
w={0}
maxW={'560px'}
p={'0 !important'}
insertProps={{
borderLeft: '1px solid',
borderLeftColor: 'myGray.200',
borderRadius: [0, '0 16px 16px 0']
}}
>
<ChatQuoteList
rawSearch={datasetCiteData.rawSearch}
metadata={datasetCiteData.metadata}
singleQuote={datasetCiteData.singleQuote}
onClose={() => setCiteModalData(undefined)}
/>
</PageContainer>
)}
</Flex>
</Watermark>
</>
);
};
const Render = (props: Props) => {
const { t } = useTranslation();
const { toast } = useToast();
const { shareId, authToken, customUid, appId } = props;
const { localUId, setLocalUId, loaded } = useShareChatStore();
const {
source,
chatId,
appId: chatStoreAppId,
setSource,
setAppId,
setOutLinkAuthData,
loaded: chatStoreLoaded
} = useChatStore();
const outLinkUid = authToken || customUid || localUId || '';
const chatHistoryProviderParams = useMemoEnhance<GetHistoriesBodyType>(() => {
return {
outLinkAuthData: {
shareId,
outLinkUid
}
};
}, [outLinkUid, shareId]);
const outLinkAuthData = useMemoEnhance(() => {
return {
shareId,
outLinkUid
};
}, [outLinkUid, shareId]);
const chatRecordProviderParams = useMemoEnhance(() => {
return {
outLinkAuthData,
chatId,
type: GetChatTypeEnum.outLink
};
}, [outLinkAuthData, chatId]);
useEffect(() => {
if (!chatStoreLoaded) return;
setSource('share');
}, [chatStoreLoaded, setSource]);
// Set default localUId
useEffect(() => {
if (loaded) {
if (!localUId) {
setLocalUId(`shareChat-${Date.now()}-${getNanoid(24)}`);
}
}
}, [loaded, localUId, setLocalUId]);
// Init outLinkAuthData
useEffect(() => {
if (!chatStoreLoaded || !outLinkAuthData.outLinkUid) return;
setOutLinkAuthData(outLinkAuthData);
return () => {
setOutLinkAuthData({});
};
}, [chatStoreLoaded, outLinkAuthData, setOutLinkAuthData]);
// Watch appId
useEffect(() => {
if (!chatStoreLoaded) return;
setAppId(appId);
}, [appId, chatStoreLoaded, setAppId]);
useMount(() => {
if (!appId) {
toast({
status: 'warning',
title: t('chat:invalid_share_url')
});
}
});
const isCurrentChatLinkReady =
chatStoreLoaded &&
source === ChatSourceEnum.share &&
chatStoreAppId === appId &&
outLinkAuthData.shareId === shareId &&
outLinkAuthData.outLinkUid === outLinkUid &&
!!appId &&
!!outLinkUid;
return isCurrentChatLinkReady ? (
<ChatContextProvider params={chatHistoryProviderParams}>
<ChatItemContextProvider
showRouteToDatasetDetail={false}
showWholeResponse={false}
canDownloadSource={props.canDownloadSource}
isShowCite={props.isShowCite}
isShowFullText={props.isShowFullText}
showRunningStatus={props.showRunningStatus}
showSkillReferences={props.showSkillReferences}
>
<ChatRecordContextProvider params={chatRecordProviderParams}>
<OutLink {...props} />
</ChatRecordContextProvider>
</ChatItemContextProvider>
</ChatContextProvider>
) : (
<NextHead title={props.appName} desc={props.appIntro} icon={props.appAvatar} />
);
};
export default React.memo(Render);
export async function getServerSideProps(context: any) {
const shareId = context?.query?.shareId || '';
const authToken = context?.query?.authToken || '';
const customUid = context?.query?.customUid || '';
const app = await (async () => {
try {
return MongoOutLink.findOne(
{
shareId
},
'appId canDownloadSource showCite showFullText showRunningStatus showSkillReferences'
)
.populate<{ associatedApp: AppSchemaType }>('associatedApp', 'name avatar intro')
.lean();
} catch (error) {
logger.error('getServerSideProps failed', {
error,
shareId
});
return undefined;
}
})();
return {
props: {
appId: app?.appId ? String(app?.appId) : '',
appName: app?.associatedApp?.name ?? 'AI',
appAvatar: app?.associatedApp?.avatar ?? '',
appIntro: app?.associatedApp?.intro ?? 'AI',
canDownloadSource: app?.canDownloadSource ?? false,
isShowCite: app?.showCite ?? false,
isShowFullText: app?.showFullText ?? false,
showRunningStatus: app?.showRunningStatus ?? false,
showSkillReferences: app?.showSkillReferences ?? false,
shareId: shareId ?? '',
authToken: authToken ?? '',
customUid,
...(await serviceSideProps(context, ['file', 'app', 'chat', 'workflow'], {
langCookieKey: SHARE_LANG_KEY,
fallbackLangCookieKey: LANG_KEY
}))
}
};
}
'use client';
import React, { useMemo, useState } from 'react';
import { Box, Flex, Button, useDisclosure, Input, InputGroup } from '@chakra-ui/react';
import { AddIcon } from '@chakra-ui/icons';
import { serviceSideProps } from '@/web/common/i18n/utils';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { FolderIcon } from '@fastgpt/global/common/file/image/constants';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { postCreateAppFolder } from '@/web/core/app/api/app';
import type { EditFolderFormType } from '@fastgpt/web/components/common/MyModal/EditFolderModal';
import { useContextSelector } from 'use-context-selector';
import AppListContextProvider, { AppListContext } from '@/pageComponents/dashboard/apps/context';
import FolderPath from '@/components/common/folder/Path';
import { useRouter } from 'next/router';
import FolderSlideCard from '@/components/common/folder/SlideCard';
import { delAppById, resumeInheritPer } from '@/web/core/app/api';
import { AppRoleList } from '@fastgpt/global/support/permission/app/constant';
import {
deleteAppCollaborators,
getCollaboratorList,
postUpdateAppCollaborators
} from '@/web/core/app/api/collaborator';
import type { CreateAppType } from '@/pageComponents/dashboard/apps/CreateModal';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import JsonImportModal from '@/pageComponents/dashboard/apps/JsonImportModal';
import DashboardContainer from '@/pageComponents/dashboard/Container';
import List from '@/pageComponents/dashboard/apps/List';
import MCPToolsEditModal from '@/pageComponents/dashboard/apps/MCPToolsEditModal';
import { getUtmWorkflow } from '@/web/support/marketing/utils';
import { useMount } from 'ahooks';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
const CreateModal = dynamic(() => import('@/pageComponents/dashboard/apps/CreateModal'));
const EditFolderModal = dynamic(
() => import('@fastgpt/web/components/common/MyModal/EditFolderModal')
);
const HttpEditModal = dynamic(() => import('@/pageComponents/dashboard/apps/HttpPluginEditModal'));
const MyApps = ({ MenuIcon }: { MenuIcon: JSX.Element }) => {
const { t } = useTranslation();
const router = useRouter();
const { isPc } = useSystem();
const {
paths,
parentId,
myApps,
appType,
loadMyApps,
onUpdateApp,
setMoveAppId,
isFetchingApps,
folderDetail,
refetchFolderDetail,
searchKey,
setSearchKey
} = useContextSelector(AppListContext, (v) => v);
const { userInfo } = useUserStore();
const [createAppType, setCreateAppType] = useState<CreateAppType>();
const {
isOpen: isOpenCreateHttpPlugin,
onOpen: onOpenCreateHttpPlugin,
onClose: onCloseCreateHttpPlugin
} = useDisclosure();
const {
isOpen: isOpenCreateMCPTools,
onOpen: onOpenCreateMCPTools,
onClose: onCloseCreateMCPTools
} = useDisclosure();
const [editFolder, setEditFolder] = useState<EditFolderFormType>();
const {
isOpen: isOpenJsonImportModal,
onOpen: onOpenJsonImportModal,
onClose: onCloseJsonImportModal
} = useDisclosure();
//if there is a workflow url in the session storage, open the json import modal and import the workflow
useMount(() => {
if (getUtmWorkflow()) {
onOpenJsonImportModal();
}
});
const { runAsync: onCreateFolder } = useRequest2(postCreateAppFolder, {
onSuccess() {
loadMyApps();
},
errorToast: 'Error'
});
const { runAsync: onDeleFolder } = useRequest2(delAppById, {
onSuccess(data) {
data.forEach((appId) => {
localStorage.removeItem(`app_log_keys_${appId}`);
});
router.replace({
query: {
parentId: folderDetail?.parentId
}
});
},
errorToast: 'Error'
});
const appTypeName = useMemo(() => {
const map: Record<AppTypeEnum | 'all', string> = {
all: t('common:core.module.template.Team app'),
[AppTypeEnum.simple]: t('app:type.Simple bot'),
[AppTypeEnum.workflow]: t('app:type.Workflow bot'),
[AppTypeEnum.plugin]: t('app:type.Plugin'),
[AppTypeEnum.httpPlugin]: t('app:type.Http plugin'),
[AppTypeEnum.folder]: t('common:Folder'),
[AppTypeEnum.toolSet]: t('app:type.MCP tools'),
[AppTypeEnum.tool]: t('app:type.MCP tools'),
[AppTypeEnum.hidden]: t('app:type.hidden')
};
return map[appType] || map['all'];
}, [appType, t]);
return (
<Flex flexDirection={'column'} h={'100%'}>
{paths.length > 0 && (
<Box pt={[4, 6]} pl={5}>
<FolderPath
paths={paths}
hoverStyle={{ bg: 'myGray.200' }}
forbidLastClick
onClick={(parentId) => {
router.push({
query: {
...router.query,
parentId
}
});
}}
/>
</Box>
)}
<Flex gap={5} flex={'1 0 0'} h={0}>
<Flex
flex={'1 0 0'}
flexDirection={'column'}
h={'100%'}
pr={folderDetail ? [3, 2] : [3, 6]}
pl={6}
overflowY={'auto'}
overflowX={'hidden'}
>
<Flex pt={paths.length > 0 ? 3 : [4, 6]} alignItems={'center'} gap={3}>
{isPc ? (
<Box fontSize={'lg'} color={'myGray.900'} fontWeight={500}>
{appTypeName}
</Box>
) : (
MenuIcon
)}
<Box flex={1} />
{isPc && (
<SearchInput
maxW={['auto', '250px']}
value={searchKey}
onChange={(e) => setSearchKey(e.target.value)}
placeholder={t('app:search_app')}
maxLength={30}
/>
)}
{
<MyMenu
size="md"
Button={
<Button variant={'primary'} leftIcon={<AddIcon />}>
<Box>{t('common:new_create')}</Box>
</Button>
}
menuList={[
{
children: [
{
icon: 'core/app/simpleBot',
label: t('app:type.Simple bot'),
description: t('app:type.Create simple bot tip'),
onClick: () => setCreateAppType(AppTypeEnum.simple)
},
{
icon: 'core/app/type/workflowFill',
label: t('app:type.Workflow bot'),
description: t('app:type.Create workflow tip'),
onClick: () => setCreateAppType(AppTypeEnum.workflow)
},
{
icon: 'core/app/type/pluginFill',
label: t('app:type.Plugin'),
description: t('app:type.Create one plugin tip'),
onClick: () => setCreateAppType(AppTypeEnum.plugin)
},
{
icon: 'core/app/type/httpPluginFill',
label: t('app:type.Http plugin'),
description: t('app:type.Create http plugin tip'),
onClick: onOpenCreateHttpPlugin
},
{
icon: 'core/app/type/mcpToolsFill',
label: t('app:type.MCP tools'),
description: t('app:type.Create mcp tools tip'),
onClick: onOpenCreateMCPTools
}
]
},
{
children: [
{
icon: 'core/app/type/jsonImport',
label: t('app:type.Import from json'),
description: t('app:type.Import from json tip'),
onClick: onOpenJsonImportModal
}
]
},
{
children: [
{
icon: FolderIcon,
label: t('common:Folder'),
onClick: () => setEditFolder({})
}
]
}
]}
/>
}
</Flex>
{!isPc && (
<Box mt={2}>
{
<SearchInput
maxW={['auto', '250px']}
value={searchKey}
onChange={(e) => setSearchKey(e.target.value)}
placeholder={t('app:search_app')}
maxLength={30}
/>
}
</Box>
)}
<MyBox flex={'1 0 0'} isLoading={myApps.length === 0 && isFetchingApps}>
<List />
</MyBox>
</Flex>
{/* Folder slider */}
{!!folderDetail && isPc && (
<Box pt={[4, 6]} pr={[4, 6]} h={'100%'} pb={4} overflow={'auto'}>
<FolderSlideCard
refetchResource={() => Promise.all([refetchFolderDetail(), loadMyApps()])}
resumeInheritPermission={() => resumeInheritPer(folderDetail._id)}
isInheritPermission={folderDetail.inheritPermission}
hasParent={!!folderDetail.parentId}
refreshDeps={[folderDetail._id, folderDetail.inheritPermission]}
name={folderDetail.name}
intro={folderDetail.intro}
onEdit={() => {
setEditFolder({
id: folderDetail._id,
name: folderDetail.name,
intro: folderDetail.intro
});
}}
onMove={() => setMoveAppId(folderDetail._id)}
deleteTip={t('app:confirm_delete_folder_tip')}
onDelete={() => onDeleFolder(folderDetail._id)}
managePer={{
permission: folderDetail.permission,
onGetCollaboratorList: () => getCollaboratorList(folderDetail._id),
roleList: AppRoleList,
onUpdateCollaborators: (props) =>
postUpdateAppCollaborators({
...props,
appId: folderDetail._id
}),
refreshDeps: [folderDetail._id, folderDetail.inheritPermission],
onDelOneCollaborator: async (params) =>
deleteAppCollaborators({
...params,
appId: folderDetail._id
})
}}
/>
</Box>
)}
</Flex>
{!!editFolder && (
<EditFolderModal
{...editFolder}
onClose={() => setEditFolder(undefined)}
onCreate={(data) => onCreateFolder({ ...data, parentId })}
onEdit={({ id, ...data }) => onUpdateApp(id, data)}
/>
)}
{!!createAppType && (
<CreateModal type={createAppType} onClose={() => setCreateAppType(undefined)} />
)}
{isOpenCreateHttpPlugin && <HttpEditModal onClose={onCloseCreateHttpPlugin} />}
{isOpenCreateMCPTools && <MCPToolsEditModal onClose={onCloseCreateMCPTools} />}
{isOpenJsonImportModal && <JsonImportModal onClose={onCloseJsonImportModal} />}
</Flex>
);
};
function ContextRender() {
return (
<DashboardContainer>
{({ MenuIcon }) => (
<AppListContextProvider>
<MyApps MenuIcon={MenuIcon} />
</AppListContextProvider>
)}
</DashboardContainer>
);
}
export default ContextRender;
export async function getServerSideProps(content: any) {
return {
props: {
...(await serviceSideProps(content, ['app', 'user']))
}
};
}
import React, { useCallback, useEffect } from 'react';
import { useRouter } from 'next/router';
import { postFastLogin } from '@/web/support/user/api';
import { clearToken, setToken } from '@/web/support/user/auth';
import { useUserStore } from '@/web/support/user/useUserStore';
import { serviceSideProps } from '@/web/common/i18n/utils';
import { getErrText } from '@fastgpt/global/common/error/utils';
import Loading from '@fastgpt/web/components/common/MyLoading';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useTranslation } from 'next-i18next';
import { validateRedirectUrl } from '@/web/common/utils/uri';
import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api';
import { useLoginRedirectAfterLogin } from '@/web/support/user/loginRedirect';
import type { LangEnum } from '@fastgpt/global/common/i18n/type';
import { getFastGPTSem, onFastGPTLoginSuccess } from '@/web/support/marketing/utils';
const FastLogin = ({
code,
token,
callbackUrl,
lastTmbId
}: {
code: string;
token: string;
callbackUrl: string;
lastTmbId?: string;
}) => {
const { setUserInfo } = useUserStore();
const router = useRouter();
const { toast } = useToast();
const { t, i18n } = useTranslation();
const resolveLoginRedirect = useLoginRedirectAfterLogin();
const loginSuccess = useCallback(
async (res: LoginSuccessResponseType) => {
const safeCallbackUrl = validateRedirectUrl(callbackUrl);
const targetRoute = await resolveLoginRedirect({
user: res.user,
fallbackRoute: safeCallbackUrl,
lastTmbId
});
setUserInfo(res.user);
if (targetRoute) {
setTimeout(() => {
router.push(targetRoute);
}, 100);
}
},
[callbackUrl, lastTmbId, resolveLoginRedirect, router, setUserInfo]
);
const authCode = useCallback(
async (code: string, token: string) => {
try {
const res = await postFastLogin({
code,
token,
fastgpt_sem: getFastGPTSem(),
language: i18n.language as LangEnum
});
if (!res) {
toast({
status: 'warning',
title: t('common:support.user.login.error')
});
return setTimeout(() => {
router.replace('/login');
}, 1000);
}
await onFastGPTLoginSuccess(loginSuccess, res);
} catch (error) {
toast({
status: 'warning',
title: getErrText(error, t('common:support.user.login.error'))
});
setTimeout(() => {
router.replace('/login');
}, 1000);
}
},
[i18n.language, loginSuccess, router, t, toast]
);
useEffect(() => {
clearToken();
router.prefetch(callbackUrl);
setToken(token);
setTimeout(() => {
router.push(decodeURIComponent(callbackUrl + '?token=' + token));
}, 100);
// authCode(code, token);
}, [callbackUrl, code, router, token]);
return <Loading />;
};
export async function getServerSideProps(content: any) {
return {
props: {
code: content?.query?.code || '',
token: content?.query?.token || '',
callbackUrl: content?.query?.callbackUrl || '/dashboard/agent',
lastTmbId: content?.query?.lastTmbId || '',
...(await serviceSideProps(content, ['login']))
}
};
}
export default FastLogin;
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { GET } from '@fastgpt/service/common/api/plusRequest';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
type AuthTeamTagTokenProps = {
teamId: string;
teamToken: string;
};
export function authTeamTagToken(data: AuthTeamTagTokenProps) {
return GET<{ uid: string }>('/support/user/team/tag/authTeamToken', data);
}
export async function authTeamSpaceToken({
teamId,
teamToken
}: {
teamId: string;
teamToken: string;
}) {
// get outLink and app
const [{ uid }, member] = await Promise.all([
authTeamTagToken({ teamId, teamToken }),
MongoTeamMember.findOne({ teamId, role: TeamMemberRoleEnum.owner }, 'tmbId').lean()
]);
return {
uid,
tmbId: member?._id!
};
}
import { GET, POST } from '@/web/common/api/request';
import type { createHttpPluginBody } from '@/pages/api/core/app/httpPlugin/create';
import type { UpdateHttpPluginBody } from '@/pages/api/core/app/httpPlugin/update';
import type {
FlowNodeTemplateType,
NodeTemplateListItemType
} from '@fastgpt/global/core/workflow/type/node';
import { getAppDetailById, getMyApps } from '../api';
import type { ListAppBody } from '@/pages/api/core/app/list';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { FlowNodeTemplateTypeEnum } from '@fastgpt/global/core/workflow/constants';
import type { GetPreviewNodeQuery } from '@/pages/api/core/app/plugin/getPreviewNode';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import type {
GetPathProps,
ParentIdType,
ParentTreePathItemType
} from '@fastgpt/global/common/parentFolder/type';
import type { GetSystemPluginTemplatesBody } from '@/pages/api/core/app/plugin/getSystemPluginTemplates';
import type { PluginGroupSchemaType } from '@fastgpt/service/core/app/plugin/type';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { defaultGroup } from '@fastgpt/web/core/workflow/constants';
import type { createMCPToolsBody } from '@/pages/api/core/app/mcpTools/create';
import { type McpToolConfigType } from '@fastgpt/global/core/app/type';
import type { updateMCPToolsBody } from '@/pages/api/core/app/mcpTools/update';
import type { RunMCPToolBody } from '@/pages/api/support/mcp/client/runTool';
import type { getMCPToolsBody } from '@/pages/api/support/mcp/client/getTools';
import type {
getToolVersionListProps,
getToolVersionResponse
} from '@/pages/api/core/app/plugin/getVersionList';
import type { McpGetChildrenmResponse } from '@/pages/api/core/app/mcpTools/getChildren';
/* ============ team plugin ============== */
export const getTeamPlugTemplates = async (data?: {
parentId?: ParentIdType;
searchKey?: string;
}) => {
if (data?.parentId) {
// handle get mcptools
const app = await getAppDetailById(data.parentId);
if (app.type === AppTypeEnum.toolSet) {
const children = await getMcpChildren(data.parentId);
return children.map((item) => ({
...item,
flowNodeType: FlowNodeTypeEnum.tool,
templateType: FlowNodeTemplateTypeEnum.teamApp
}));
}
}
return getMyApps(data).then((res) =>
res.map((app) => ({
tmbId: app.tmbId,
id: app._id,
pluginId: app._id,
isFolder:
app.type === AppTypeEnum.folder ||
app.type === AppTypeEnum.httpPlugin ||
app.type === AppTypeEnum.toolSet,
templateType: FlowNodeTemplateTypeEnum.teamApp,
flowNodeType:
app.type === AppTypeEnum.workflow
? FlowNodeTypeEnum.appModule
: app.type === AppTypeEnum.toolSet
? FlowNodeTypeEnum.toolSet
: FlowNodeTypeEnum.pluginModule,
avatar: app.avatar,
name: app.name,
intro: app.intro,
showStatus: false,
version: app.pluginData?.nodeVersion,
isTool: true,
sourceMember: app.sourceMember
}))
);
};
/* ============ system plugin ============== */
export const getSystemPlugTemplates = (data: GetSystemPluginTemplatesBody) =>
POST<NodeTemplateListItemType[]>('/core/app/plugin/getSystemPluginTemplates', data);
export const getPluginGroups = () => {
return Promise.resolve([defaultGroup]);
};
export const getSystemPluginPaths = (data: GetPathProps) => {
if (!data.sourceId) return Promise.resolve<ParentTreePathItemType[]>([]);
return GET<ParentTreePathItemType[]>('/core/app/plugin/path', data);
};
export const getPreviewPluginNode = (data: GetPreviewNodeQuery) =>
GET<FlowNodeTemplateType>('/core/app/plugin/getPreviewNode', data);
export const getToolVersionList = (data: getToolVersionListProps) =>
POST<getToolVersionResponse>('/core/app/plugin/getVersionList', data);
/* ============ mcp tools ============== */
export const postCreateMCPTools = (data: createMCPToolsBody) =>
POST('/core/app/mcpTools/create', data);
export const postUpdateMCPTools = (data: updateMCPToolsBody) =>
POST('/core/app/mcpTools/update', data);
export const getMCPTools = (data: getMCPToolsBody) =>
POST<McpToolConfigType[]>('/support/mcp/client/getTools', data);
export const postRunMCPTool = (data: RunMCPToolBody) =>
POST('/support/mcp/client/runTool', data, { timeout: 300000 });
export const getMcpChildren = (id: string) =>
GET<McpGetChildrenmResponse>('/core/app/mcpTools/getChildren', { id });
/* ============ http plugin ============== */
export const postCreateHttpPlugin = (data: createHttpPluginBody) =>
POST('/core/app/httpPlugin/create', data);
export const putUpdateHttpPlugin = (body: UpdateHttpPluginBody) =>
POST('/core/app/httpPlugin/update', body);
export const getApiSchemaByUrl = (url: string) =>
POST<Object>(
'/core/app/httpPlugin/getApiSchemaByUrl',
{ url },
{
timeout: 30000
}
);
import { useSystemStore } from '@/web/common/system/useSystemStore';
import {
ChatSidebarPaneEnum,
defaultCollapseStatus,
type CollapseStatusType
} from '@/pageComponents/chat/constants';
import { getChatSetting } from '@/web/core/chat/api';
import { useChatStore } from '@/web/core/chat/context/useChatStore';
import type { ChatSettingSchema } from '@fastgpt/global/core/chat/setting/type';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useRouter } from 'next/router';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { createContext } from 'use-context-selector';
type ChatSettingReturnType = ChatSettingSchema | undefined;
export type ChatSettingContextValue = {
pane: ChatSidebarPaneEnum;
handlePaneChange: (pane: ChatSidebarPaneEnum, _id?: string) => void;
collapse: CollapseStatusType;
onTriggerCollapse: () => void;
chatSettings: ChatSettingSchema | undefined;
refreshChatSetting: () => Promise<ChatSettingReturnType>;
logos: Pick<ChatSettingSchema, 'wideLogoUrl' | 'squareLogoUrl'>;
};
export const ChatSettingContext = createContext<ChatSettingContextValue>({
pane: ChatSidebarPaneEnum.HOME,
handlePaneChange: () => {},
collapse: defaultCollapseStatus,
onTriggerCollapse: () => {},
chatSettings: undefined,
refreshChatSetting: function (): Promise<ChatSettingReturnType> {
throw new Error('Function not implemented.');
},
logos: {
wideLogoUrl: '',
squareLogoUrl: ''
}
});
export const ChatSettingContextProvider = ({ children }: { children: React.ReactNode }) => {
const router = useRouter();
const { feConfigs } = useSystemStore();
const { appId, setLastPane, setLastChatAppId, lastPane } = useChatStore();
const { pane = lastPane || ChatSidebarPaneEnum.HOME } = router.query as {
pane: ChatSidebarPaneEnum;
};
const [collapse, setCollapse] = useState<CollapseStatusType>(defaultCollapseStatus);
const { data: chatSettings, runAsync: refreshChatSetting } = useRequest2(
async () => {
if (!feConfigs.isPlus) return;
return await getChatSetting();
},
{
manual: false,
refreshDeps: [feConfigs.isPlus],
onSuccess(data) {
if (!data) return;
// Reset home page appId
if (pane === ChatSidebarPaneEnum.HOME && appId !== data.appId) {
handlePaneChange(ChatSidebarPaneEnum.HOME, data.appId);
}
}
}
);
const handlePaneChange = useCallback(
async (newPane: ChatSidebarPaneEnum, id?: string) => {
if (newPane === pane && !id) return;
const _id = (() => {
if (id) return id;
const hiddenAppId = chatSettings?.appId;
if (newPane === ChatSidebarPaneEnum.HOME && hiddenAppId) {
return hiddenAppId;
}
return '';
})();
await router.replace({
query: {
appId: _id,
pane: newPane
}
});
setLastPane(newPane);
setLastChatAppId(_id);
},
[pane, router, setLastPane, setLastChatAppId, chatSettings?.appId]
);
useEffect(() => {
if (!Object.values(ChatSidebarPaneEnum).includes(pane)) {
handlePaneChange(ChatSidebarPaneEnum.HOME);
}
}, [pane]);
const logos: Pick<ChatSettingSchema, 'wideLogoUrl' | 'squareLogoUrl'> = useMemo(
() => ({
wideLogoUrl: chatSettings?.wideLogoUrl,
squareLogoUrl: chatSettings?.squareLogoUrl
}),
[chatSettings?.squareLogoUrl, chatSettings?.wideLogoUrl]
);
const value: ChatSettingContextValue = useMemo(
() => ({
pane,
handlePaneChange,
collapse,
onTriggerCollapse: () => setCollapse(collapse === 0 ? 1 : 0),
chatSettings,
refreshChatSetting,
logos
}),
[pane, handlePaneChange, collapse, chatSettings, refreshChatSetting, logos]
);
return <ChatSettingContext.Provider value={value}>{children}</ChatSettingContext.Provider>;
};
......@@ -29,7 +29,7 @@ export type DatasetPermissionState = {
export const useDatasetPermission = (datasetId: string, autoCheck = true) => {
const { data, loading, error, run } = useRequest2(() => checkDatasetPermission({ datasetId }), {
manual: !autoCheck,
onError: (err) => {
onError: (err: any) => {
console.error('Failed to fetch dataset permission:', err);
}
});
......
......@@ -26,6 +26,7 @@
"**/*.test.tsx",
"../../packages/**/vitest*.config.ts",
"../../packages/**/test/**",
"../../packages/plugins/**",
".next",
"dist",
"coverage"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment