FastGPT-03-Web组件与主应用
第一部分:packages/web - Web组件层
模块概览
职责:提供可复用的 UI 组件、React Hooks、国际化工具,供主应用(projects/app)使用。
目录结构:
packages/web/
├── common/ # 通用工具(fetch、文件、Zustand)
├── components/ # UI组件(按钮、表单、图标等)
├── context/ # React Context(系统状态)
├── hooks/ # 自定义 Hooks
├── i18n/ # 国际化配置和翻译
├── store/ # 全局状态管理(Zustand)
└── styles/ # 主题和样式
核心组件
1. 通用 UI 组件
MyIcon(图标组件):
// components/common/Icon/index.tsx
import { Icon as ChakraIcon } from '@chakra-ui/react';
import type { IconProps } from '@chakra-ui/react';
import { iconPaths } from './constants';
export const MyIcon = ({
name,
width = '1em',
height = '1em',
...props
}: {
name: string;
} & IconProps) => {
const path = iconPaths[name];
if (!path) {
console.warn(`Icon "${name}" not found`);
return null;
}
return (
<ChakraIcon viewBox="0 0 1024 1024" {...props} w={width} h={height}>
<path d={path} fill="currentColor" />
</ChakraIcon>
);
};
MyTooltip(提示组件):
// components/common/MyTooltip/index.tsx
import { Tooltip } from '@chakra-ui/react';
export const MyTooltip = ({ label, children, ...props }) => {
return (
<Tooltip
label={label}
bg="gray.700"
color="white"
fontSize="sm"
px={3}
py={2}
borderRadius="md"
hasArrow
{...props}
>
{children}
</Tooltip>
);
};
2. 自定义 Hooks
useRequest(封装异步请求):
// hooks/useRequest.tsx
import { useState, useCallback } from 'react';
import { useToast } from './useToast';
export const useRequest = <T = any, P extends any[] = any[]>({
fetcher,
onSuccess,
onError,
successToast = '',
errorToast = ''
}: {
fetcher: (...args: P) => Promise<T>;
onSuccess?: (data: T) => void;
onError?: (error: any) => void;
successToast?: string;
errorToast?: string;
}) => {
const [loading, setLoading] = useState(false);
const { toast } = useToast();
const run = useCallback(
async (...args: P) => {
setLoading(true);
try {
const data = await fetcher(...args);
onSuccess?.(data);
if (successToast) {
toast({ title: successToast, status: 'success' });
}
return data;
} catch (error) {
onError?.(error);
if (errorToast) {
toast({ title: errorToast, status: 'error' });
}
throw error;
} finally {
setLoading(false);
}
},
[fetcher, onSuccess, onError]
);
return { loading, run };
};
useConfirm(确认对话框):
// hooks/useConfirm.tsx
import { useDisclosure } from '@chakra-ui/react';
import { useCallback, useRef } from 'react';
export const useConfirm = ({
title = '提示',
content,
onConfirm
}: {
title?: string;
content: string;
onConfirm: () => void | Promise<void>;
}) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const callbackRef = useRef(onConfirm);
callbackRef.current = onConfirm;
const openConfirm = useCallback(() => {
onOpen();
}, [onOpen]);
const ConfirmModal = useCallback(() => {
return (
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>{title}</ModalHeader>
<ModalBody>{content}</ModalBody>
<ModalFooter>
<Button onClick={onClose}>取消</Button>
<Button
ml={3}
colorScheme="red"
onClick={async () => {
await callbackRef.current();
onClose();
}}
>
确认
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}, [isOpen, onClose, title, content]);
return { openConfirm, ConfirmModal };
};
3. 国际化(i18n)
配置:
// i18n/utils.ts
import { useTranslation } from 'react-i18next';
export const useI18n = () => {
const { t, i18n } = useTranslation();
return {
t,
currentLang: i18n.language,
changeLanguage: (lang: string) => i18n.changeLanguage(lang)
};
};
// 翻译key生成
export const i18nT = (key: string, options?: any) => {
return key; // 在运行时由 i18next 处理
};
翻译文件结构:
i18n/
├── en/
│ ├── common.json
│ ├── app.json
│ ├── chat.json
│ └── ...
└── zh-CN/
├── common.json
├── app.json
├── chat.json
└── ...
第二部分:projects/app - 主应用
模块概览
职责:FastGPT 的主应用,基于 Next.js 构建,包含前端页面和后端 API Routes。
技术栈:
- Next.js 14(SSR + API Routes)
- React 18
- Chakra UI(UI框架)
- React Query(数据获取)
- Zustand(状态管理)
目录结构:
projects/app/
├── public/ # 静态资源
├── src/
│ ├── components/ # 应用组件
│ ├── global/ # 全局配置
│ ├── pageComponents/ # 页面组件
│ ├── pages/ # Next.js页面和API路由
│ ├── service/ # 服务层(调用@fastgpt/service)
│ ├── types/ # 类型定义
│ └── web/ # Web工具
└── data/ # 配置文件(config.json、model.json)
核心 API Routes
1. 对话 API
v2/chat/completions(OpenAI 兼容接口):
// pages/api/v2/chat/completions.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { authChatCert } from '@fastgpt/service/support/permission/auth/chat';
import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
import { saveChat } from '@fastgpt/service/core/chat/saveChat';
type AuthResponseType = {
teamId: string;
tmbId: string;
appId: string;
app: AppSchema;
canWrite: boolean;
};
async function handler(req: NextApiRequest, res: NextApiResponse) {
const {
chatId,
appId,
stream = true,
detail = false,
messages,
variables = {}
} = req.body;
// 1. 鉴权
const { teamId, tmbId, app, canWrite }: AuthResponseType = await authChatCert({
req,
authToken: true,
appId
});
// 2. 提取用户输入
const userChatInput = messages[messages.length - 1].content;
// 3. 初始化对话(获取历史)
const { histories } = await initChat({
appId,
chatId,
teamId,
tmbId
});
// 4. 执行工作流
const { flowResponses, flowUsages, assistantResponses, newVariables } = await dispatchWorkFlow({
res: stream ? res : undefined,
stream,
detail,
runningUserInfo: { teamId, tmbId },
runningAppInfo: { id: appId, name: app.name },
runtimeNodes: app.nodes,
runtimeEdges: app.edges,
histories,
variables: {
...variables,
cTime: new Date().toISOString(),
appId
},
chatConfig: app.chatConfig,
mode: 'chat',
usageSource: 'fastgpt'
});
// 5. 保存对话
await saveChat({
chatId,
appId,
teamId,
tmbId,
nodes: app.nodes,
appChatConfig: app.chatConfig,
variables: newVariables,
userContent: {
obj: 'Human',
value: [{ type: 'text', text: { content: userChatInput } }]
},
aiContent: {
obj: 'AI',
value: assistantResponses
},
durationSeconds: flowUsages.reduce((sum, u) => sum + (u.duration || 0), 0)
});
// 6. 返回响应
if (stream) {
res.end();
} else {
res.json({
id: chatId,
model: app.chatConfig.model,
usage: {
prompt_tokens: flowUsages.reduce((sum, u) => sum + u.inputTokens, 0),
completion_tokens: flowUsages.reduce((sum, u) => sum + u.outputTokens, 0),
total_tokens: flowUsages.reduce((sum, u) => sum + u.totalTokens, 0)
},
choices: [
{
message: {
role: 'assistant',
content: assistantResponses
.filter(r => r.type === 'text')
.map(r => r.text?.content)
.join('\n')
},
finish_reason: 'stop'
}
]
});
}
}
export default handler;
关键逻辑:
- 鉴权(
authChatCert):验证 API Key 或 Token,获取用户信息 - 初始化对话:加载历史记录和上下文
- 工作流执行:调用 Service 层的
dispatchWorkFlow - 保存对话:存储用户消息和 AI 回复
- 流式响应:通过 SSE 实时推送(stream=true)
- 非流式响应:返回完整的 OpenAI 格式 JSON(stream=false)
2. 知识库 API
dataset/collection/create(上传文件创建集合):
// pages/api/core/dataset/collection/create.ts
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
import { uploadFile } from '@fastgpt/service/common/file/gridfs/controller';
import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/schema';
import { fileParseQueue } from '@fastgpt/service/worker/controller';
async function handler(req: ApiRequestProps) {
const { datasetId, name, mode = 'auto' } = req.body;
const file = req.file; // Multer 上传的文件
// 1. 验证权限
const { dataset } = await authDataset({
req,
authToken: true,
authApiKey: true,
datasetId,
per: 'w'
});
// 2. 上传文件到 GridFS/S3
const { fileId } = await uploadFile({
teamId: dataset.teamId,
file: {
originalname: file.originalname,
buffer: file.buffer
}
});
// 3. 创建 Collection
const collection = await MongoDatasetCollection.create({
teamId: dataset.teamId,
datasetId: dataset._id,
name: name || file.originalname,
type: DatasetCollectionTypeEnum.file,
fileId,
trainingType: mode,
chunkSettings: dataset.chunkSettings
});
// 4. 发送文件解析任务到队列
await fileParseQueue.add('parse', {
collectionId: String(collection._id),
datasetId: String(dataset._id),
teamId: dataset.teamId,
fileId,
filePath: file.originalname
});
return {
collectionId: collection._id
};
}
export default withNextCors(
NextAPI(handler, {
upload: {
maxFileSize: 100 * 1024 * 1024 // 100MB
}
})
);
3. 应用管理 API
app/create(创建应用):
// pages/api/core/app/create.ts
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { MongoApp } from '@fastgpt/service/core/app/schema';
async function handler(req: ApiRequestProps) {
const { name, avatar, type = 'simple' } = req.body;
// 1. 鉴权
const { teamId, tmbId } = await authUserPer({
req,
authToken: true,
authApiKey: true,
per: 'w'
});
// 2. 创建应用(包含默认工作流节点)
const defaultNodes = getDefaultNodes(type);
const defaultEdges = getDefaultEdges(type);
const app = await MongoApp.create({
teamId,
tmbId,
name,
avatar: avatar || '/icon/logo.svg',
type,
nodes: defaultNodes,
edges: defaultEdges,
chatConfig: getDefaultChatConfig()
});
return {
appId: app._id
};
}
// 获取默认节点(根据应用类型)
const getDefaultNodes = (type: AppTypeEnum) => {
if (type === 'simple') {
return [
{
nodeId: 'workflowStartNodeId',
flowNodeType: FlowNodeTypeEnum.workflowStart,
name: '流程开始',
position: { x: 300, y: 300 },
inputs: [
{
key: NodeInputKeyEnum.userChatInput,
renderTypeList: [FlowNodeInputTypeEnum.reference],
valueType: WorkflowIOValueTypeEnum.string,
label: '用户问题'
}
],
outputs: [
{
id: 'userChatInput',
key: NodeInputKeyEnum.userChatInput,
label: '用户问题',
valueType: WorkflowIOValueTypeEnum.string
}
]
},
{
nodeId: 'aiChatNodeId',
flowNodeType: FlowNodeTypeEnum.chatNode,
name: 'AI 对话',
position: { x: 800, y: 300 },
inputs: [
{
key: NodeInputKeyEnum.aiModel,
renderTypeList: [FlowNodeInputTypeEnum.selectLLMModel],
value: 'gpt-3.5-turbo'
},
{
key: NodeInputKeyEnum.aiSystemPrompt,
renderTypeList: [FlowNodeInputTypeEnum.textarea],
value: '你是一个友好的AI助手。'
},
{
key: NodeInputKeyEnum.userChatInput,
renderTypeList: [FlowNodeInputTypeEnum.reference],
value: ['workflowStartNodeId', NodeInputKeyEnum.userChatInput]
}
]
}
];
}
// 其他类型...
};
核心页面组件
1. 工作流编辑器
位置:src/pageComponents/app/EditNodes/index.tsx
基于 ReactFlow 实现的可视化工作流编辑器:
import ReactFlow, {
Node,
Edge,
useNodesState,
useEdgesState,
addEdge,
Background,
Controls
} from 'reactflow';
import 'reactflow/dist/style.css';
export const WorkflowEditor = ({ appId }: { appId: string }) => {
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
// 加载应用数据
useEffect(() => {
fetchAppDetail(appId).then(app => {
setNodes(app.nodes.map(nodeToFlowNode));
setEdges(app.edges.map(edgeToFlowEdge));
});
}, [appId]);
// 连接节点
const onConnect = useCallback((params) => {
setEdges((eds) => addEdge(params, eds));
}, []);
// 保存工作流
const handleSave = async () => {
await updateApp({
appId,
nodes: nodes.map(flowNodeToNode),
edges: edges.map(flowEdgeToEdge)
});
};
return (
<Box h="100vh">
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={customNodeTypes}
fitView
>
<Background />
<Controls />
</ReactFlow>
<Button onClick={handleSave}>保存</Button>
</Box>
);
};
// 自定义节点组件
const customNodeTypes = {
[FlowNodeTypeEnum.chatNode]: ChatNodeCard,
[FlowNodeTypeEnum.datasetSearchNode]: DatasetSearchNodeCard,
[FlowNodeTypeEnum.agent]: AgentNodeCard,
// ... 其他节点类型
};
节点卡片组件示例:
const ChatNodeCard = ({ data, id }: NodeProps) => {
const { updateNodeInputs } = useWorkflowStore();
return (
<Box
border="1px solid"
borderColor="gray.300"
borderRadius="md"
p={4}
bg="white"
minW="300px"
>
<Flex align="center" mb={3}>
<MyIcon name="chat" mr={2} />
<Text fontWeight="bold">AI 对话</Text>
</Flex>
{/* 输入配置 */}
{data.inputs.map((input) => (
<FormControl key={input.key} mb={3}>
<FormLabel>{input.label}</FormLabel>
{input.renderTypeList.includes('select') ? (
<Select
value={input.value}
onChange={(e) =>
updateNodeInputs(id, input.key, e.target.value)
}
>
{/* 选项 */}
</Select>
) : (
<Input
value={input.value}
onChange={(e) =>
updateNodeInputs(id, input.key, e.target.value)
}
/>
)}
</FormControl>
))}
{/* 连接点(Handles) */}
<Handle type="target" position="left" id="target" />
<Handle type="source" position="right" id="source" />
</Box>
);
};
2. 对话界面
位置:src/pageComponents/chat/index.tsx
export const ChatPage = ({ appId, chatId }: { appId: string; chatId?: string }) => {
const [messages, setMessages] = useState<ChatItemType[]>([]);
const [inputValue, setInputValue] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
// 加载历史消息
useEffect(() => {
if (chatId) {
fetchChatHistory({ appId, chatId }).then(setMessages);
}
}, [appId, chatId]);
// 发送消息
const handleSend = async () => {
if (!inputValue.trim()) return;
const newMessage: ChatItemType = {
obj: 'Human',
value: [{ type: 'text', text: { content: inputValue } }]
};
setMessages([...messages, newMessage]);
setInputValue('');
setIsStreaming(true);
try {
// SSE 流式请求
const response = await fetch('/api/v2/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`
},
body: JSON.stringify({
chatId: chatId || nanoid(),
appId,
stream: true,
messages: [
...messages.map(msgToOpenAI),
{ role: 'user', content: inputValue }
]
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let aiMessage = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter((line) => line.trim());
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6));
const content = data.choices[0]?.delta?.content || '';
aiMessage += content;
// 实时更新 AI 消息
setMessages((msgs) => {
const lastMsg = msgs[msgs.length - 1];
if (lastMsg.obj === 'AI') {
return [
...msgs.slice(0, -1),
{
...lastMsg,
value: [
{
type: 'text',
text: { content: aiMessage }
}
]
}
];
} else {
return [
...msgs,
{
obj: 'AI',
value: [
{
type: 'text',
text: { content: aiMessage }
}
]
}
];
}
});
}
}
}
} catch (error) {
console.error('发送消息失败', error);
} finally {
setIsStreaming(false);
}
};
return (
<Flex direction="column" h="100vh">
{/* 消息列表 */}
<Box flex={1} overflowY="auto" p={4}>
{messages.map((msg, index) => (
<ChatMessage key={index} message={msg} />
))}
</Box>
{/* 输入框 */}
<Flex p={4} borderTop="1px solid" borderColor="gray.200">
<Textarea
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="输入消息..."
resize="none"
mr={2}
/>
<Button
onClick={handleSend}
isLoading={isStreaming}
colorScheme="blue"
>
发送
</Button>
</Flex>
</Flex>
);
};
状态管理
使用 Zustand 管理全局状态:
// web/store/useCommonStore.ts
import create from 'zustand';
import { persist } from 'zustand/middleware';
interface CommonState {
// 用户信息
userInfo: UserType | null;
setUserInfo: (user: UserType | null) => void;
// 团队信息
teamInfo: TeamType | null;
setTeamInfo: (team: TeamType | null) => void;
// 模型配置
modelConfig: ModelConfigType[];
setModelConfig: (config: ModelConfigType[]) => void;
// 系统配置
systemConfig: SystemConfigType | null;
setSystemConfig: (config: SystemConfigType) => void;
}
export const useCommonStore = create<CommonState>()(
persist(
(set) => ({
userInfo: null,
setUserInfo: (user) => set({ userInfo: user }),
teamInfo: null,
setTeamInfo: (team) => set({ teamInfo: team }),
modelConfig: [],
setModelConfig: (config) => set({ modelConfig: config }),
systemConfig: null,
setSystemConfig: (config) => set({ systemConfig: config })
}),
{
name: 'fastgpt-common-storage',
partialize: (state) => ({
userInfo: state.userInfo,
teamInfo: state.teamInfo
})
}
)
);
关键时序图
用户对话流程(前端到后端)
sequenceDiagram
autonumber
participant User as 用户
participant UI as 前端页面
participant API as API Routes
participant Service as Service层
participant DB as 数据库
User->>UI: 输入消息并点击发送
UI->>UI: 更新消息列表(本地)
UI->>API: POST /api/v2/chat/completions<br/>(SSE stream)
API->>API: 鉴权验证
API->>Service: initChat(获取历史)
Service->>DB: 查询历史消息
DB-->>Service: 返回历史
Service-->>API: 历史消息
API->>Service: dispatchWorkflow(执行工作流)
Service->>Service: 执行节点(AI、检索、Agent等)
loop 流式推送
Service-->>API: SSE chunk
API-->>UI: data: {content}
UI->>UI: 实时渲染AI回复
end
Service-->>API: 工作流执行完成
API->>Service: saveChat(保存对话)
Service->>DB: 存储对话记录
DB-->>Service: 保存成功
Service-->>API: 保存完成
API-->>UI: 流结束
UI->>UI: 标记消息完成
工作流编辑与保存流程
sequenceDiagram
autonumber
participant User as 用户
participant Editor as 工作流编辑器
participant State as React State
participant API as API Routes
participant DB as MongoDB
User->>Editor: 打开应用编辑页面
Editor->>API: GET /api/core/app/detail?appId=xxx
API->>DB: 查询应用数据
DB-->>API: 返回应用(nodes, edges)
API-->>Editor: 应用数据
Editor->>State: setNodes, setEdges
User->>Editor: 拖拽添加新节点
Editor->>State: addNode(newNode)
State-->>Editor: 更新节点列表
User->>Editor: 连接节点
Editor->>State: addEdge(newEdge)
State-->>Editor: 更新边列表
User->>Editor: 点击保存按钮
Editor->>API: POST /api/core/app/update<br/>{nodes, edges}
API->>DB: 更新应用文档
DB-->>API: 更新成功
API-->>Editor: 保存成功
Editor->>Editor: 显示成功提示
配置文件
data/config.json
系统配置文件(运行时配置):
{
"FeConfig": {
"show_emptyChat": true,
"show_register": false,
"show_appStore": true,
"show_userDetail": true,
"show_git": true,
"systemTitle": "FastGPT",
"systemDescription": "AI Agent 构建平台",
"concatMd": "",
"limit": {
"exportDatasetLimitMinutes": 0,
"websiteSyncLimitMinuted": 0
},
"scripts": []
},
"SystemParams": {
"pluginBaseUrl": "",
"openapiPrefix": "/api"
}
}
data/model.json
模型配置文件:
[
{
"provider": "OpenAI",
"model": "gpt-4",
"name": "GPT-4",
"type": "llm",
"maxContext": 8000,
"maxResponse": 4000,
"quoteMaxToken": 3000,
"vision": false,
"functionCall": true,
"toolChoice": true,
"inputPrice": 30,
"outputPrice": 60
},
{
"provider": "OpenAI",
"model": "text-embedding-ada-002",
"name": "Embedding-2",
"type": "embedding",
"defaultToken": 512,
"maxToken": 8191,
"weight": 100,
"inputPrice": 0.1
}
]
部署与构建
构建命令
# 开发环境
pnpm dev
# 构建生产版本
pnpm build
# 启动生产服务
pnpm start
环境变量
# MongoDB
MONGODB_URI=mongodb://localhost:27017/fastgpt
# PostgreSQL(向量数据库)
PG_URL=postgresql://user:pass@localhost:5432/postgres
# Redis
REDIS_URL=redis://localhost:6379
# S3
S3_ENDPOINT=http://localhost:9000
S3_ACCESS_KEY=minioadmin
S3_SECRET_KEY=minioadmin
S3_BUCKET_NAME=fastgpt
# OneAPI
ONEAPI_URL=http://localhost:3000/v1
ONEAPI_KEY=sk-xxx
# NextAuth
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-secret-key
# Root Key(管理员密钥)
ROOT_KEY=your-root-key
Docker 部署
# Dockerfile
FROM node:20-alpine AS base
RUN npm install -g pnpm
FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]
总结与最佳实践
前端开发规范
- 组件复用:通用组件放在
packages/web,页面特定组件放在pageComponents - 状态管理:全局状态用 Zustand,局部状态用 React State
- 数据获取:使用 React Query 管理服务端状态
- 类型安全:所有 API 请求和响应定义 TypeScript 类型
- 国际化:所有文案使用 i18n 键,不硬编码
API 开发规范
- 鉴权:所有 API 必须先调用鉴权函数(
authXXX) - 错误处理:使用 try-catch 捕获错误,返回标准错误格式
- 响应格式:使用
jsonRes统一响应格式 - 日志记录:关键操作记录日志(创建、更新、删除)
- 性能优化:数据库查询使用索引,避免 N+1 查询
性能优化
- 代码分割:使用 Next.js 的动态导入
- 图片优化:使用 Next.js Image 组件
- 缓存策略:静态资源使用 CDN,API 响应使用 SWR
- SSR优化:首屏使用 SSR,后续使用 CSR