mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-18 18:14:19 +00:00
feat: admin image
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -28,4 +28,5 @@ next-env.d.ts
|
||||
platform.json
|
||||
testApi/
|
||||
local/
|
||||
.husky/
|
||||
.husky/
|
||||
dist/
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -3,5 +3,4 @@
|
||||
"editor.mouseWheelZoom": true,
|
||||
"typescript.tsdk": "./client/node_modules/typescript/lib",
|
||||
"prettier.prettierPath": "./node_modules/prettier"
|
||||
|
||||
}
|
11
admin/.dockerignore
Normal file
11
admin/.dockerignore
Normal file
@@ -0,0 +1,11 @@
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
node_modules
|
||||
npm-debug.log
|
||||
README.md
|
||||
.git
|
||||
|
||||
.yalc/
|
||||
yalc.lock
|
||||
testApi/
|
||||
node_modules
|
6
admin/.env.template
Normal file
6
admin/.env.template
Normal file
@@ -0,0 +1,6 @@
|
||||
MONGODB_URI=mongodb://username:psw@0.0.0.0:27017/?authSource=admin
|
||||
MONGODB_NAME=fastgpt
|
||||
ADMIN_USER=username
|
||||
ADMIN_PASS=password
|
||||
ADMIN_SECRET=any
|
||||
VITE_PUBLIC_SERVER_URL=http://localhost:3001 # 和server.js一致
|
1
admin/.gitignore
vendored
Normal file
1
admin/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules/
|
48
admin/Dockerfile
Normal file
48
admin/Dockerfile
Normal file
@@ -0,0 +1,48 @@
|
||||
# Install dependencies only when needed
|
||||
FROM node:current-alpine AS builder
|
||||
RUN npm config set registry https://registry.npmmirror.com/
|
||||
RUN apk add --no-cache libc6-compat && npm install -g pnpm
|
||||
RUN pnpm config set registry https://registry.npmmirror.com/
|
||||
|
||||
WORKDIR /app
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
ENV VITE_PUBLIC_SERVER_URL ''
|
||||
# Install dependencies based on the preferred package manager
|
||||
COPY . .
|
||||
RUN \
|
||||
[ -f pnpm-lock.yaml ] && pnpm install || \
|
||||
(echo "Lockfile not found." && exit 1)
|
||||
|
||||
RUN pnpm build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM node:current-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
RUN sed -i 's/https/http/' /etc/apk/repositories
|
||||
RUN apk add curl \
|
||||
&& apk add ca-certificates \
|
||||
&& update-ca-certificates
|
||||
|
||||
COPY package.json pnpm-lock.yaml* ./
|
||||
COPY --from=builder /app/server.js ./server.js
|
||||
COPY --from=builder /app/service ./service
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
RUN npm config set registry https://registry.npmmirror.com/
|
||||
RUN npm install -g pnpm
|
||||
RUN pnpm config set registry https://registry.npmmirror.com/
|
||||
RUN pnpm install --prod
|
||||
RUN npm remove -g pnpm
|
||||
|
||||
ENV PORT=3001
|
||||
|
||||
EXPOSE 3001
|
||||
|
||||
CMD ["node", "server.js"]
|
@@ -1,15 +1,41 @@
|
||||
## fastgpt-admin
|
||||
原作地址:https://github.com/c121914yu/FastGPT/
|
||||
# FastGpt Admin
|
||||
|
||||
## 项目原理
|
||||
使用tushan项目做前端,然后构造了一个与mongodb做沟通的API做后端,可以做到创建、修改和删除用户
|
||||
|
||||
## 使用方法
|
||||
1. 修改根目录下的server.js文件中的mongodb数据库连接地址。
|
||||
2. pnpm i && pnpm dev
|
||||
3. 默认账号密码为tushan
|
||||
使用 tushan 项目做前端,然后构造了一个与 mongodb 做沟通的 API 做后端,可以做到创建、修改和删除用户
|
||||
|
||||
## 开发
|
||||
|
||||
## 可能会有的功能
|
||||
1. 对接数据库中的其他数据
|
||||
2. tokens充值功能
|
||||
1. 复制 .env.template 文件,添加环境变量
|
||||
2. pnpm i
|
||||
3. pnpm dev
|
||||
|
||||
## 部署
|
||||
|
||||
1. 本地打包
|
||||
|
||||
`docker build -t registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-admin:latest . --network host --build-arg HTTP_PROXY=http://127.0.0.1:7890 --build-arg HTTPS_PROXY=http://127.0.0.1:7890`
|
||||
|
||||
2. 直接拉镜像: `registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-admin:latest`
|
||||
3. 部署时候填写环境变量: 数据库同 FastGpt 一致
|
||||
|
||||
```
|
||||
MONGODB_URI=mongodb://username:psw@0.0.0.0:27017/?authSource=admin
|
||||
MONGODB_NAME=fastgpt
|
||||
ADMIN_USER=username
|
||||
ADMIN_PASS=password
|
||||
ADMIN_SECRET=any
|
||||
VITE_PUBLIC_SERVER_URL=http://localhost:3001 # 和server.js一致
|
||||
```
|
||||
|
||||
## sealos 部署
|
||||
|
||||
1. 进入 sealos 官网: https://cloud.sealos.io/
|
||||
2. 打开 App Launchpad(应用管理) 工具
|
||||
3. 新建应用
|
||||
1. 镜像名: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-admin:latest
|
||||
2. 容器端口: 3001
|
||||
3. 环境变量: 参考上面
|
||||
4. 打开外网访问开关
|
||||
4. 点击部署。 完成后大约等待 1 分钟,
|
||||
5. 点击 sealos 提供的外网访问地址,可以直接访问。
|
||||
|
@@ -8,23 +8,31 @@
|
||||
"dev": "concurrently \"vite\" \"npm run start:api\"",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"start:api": "node server.js"
|
||||
"start:api": "nodemon server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"concurrently": "^8.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"dayjs": "^1.11.8",
|
||||
"dotenv": "^16.1.4",
|
||||
"express": "^4.18.2",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"mongoose": "^7.2.2",
|
||||
"react": "^18.2.0",
|
||||
"react-admin": "^4.11.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tushan": "^0.2.13"
|
||||
"tushan": "^0.2.22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsonexport": "^3.0.2",
|
||||
"@types/lodash-es": "^4.17.7",
|
||||
"@types/node": "^20.2.5",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/react-helmet": "^6.1.6",
|
||||
"@types/styled-components": "^5.1.26",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"typescript": "^4.9.3",
|
||||
"typescript": "^4.9.2",
|
||||
"vite": "^4.2.1"
|
||||
}
|
||||
}
|
||||
|
5456
admin/pnpm-lock.yaml
generated
Normal file
5456
admin/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
330
admin/server.js
330
admin/server.js
@@ -1,329 +1,21 @@
|
||||
import express from 'express';
|
||||
import mongoose from 'mongoose';
|
||||
import cors from 'cors';
|
||||
import { useUserRoute } from './service/route/user.js';
|
||||
import { useAppRoute } from './service/route/app.js';
|
||||
import { useKbRoute } from './service/route/kb.js';
|
||||
import { useSystemRoute } from './service/route/system.js';
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.static('dist'));
|
||||
|
||||
const mongoURI = '';//在这里填入mongodb的连接地址
|
||||
mongoose.connect(mongoURI, { useNewUrlParser: true, useUnifiedTopology: true })
|
||||
.then(() => console.log('Connected to MongoDB successfully!'))
|
||||
.catch((err) => console.log(`Error connecting to MongoDB: ${err}`));
|
||||
useUserRoute(app);
|
||||
useAppRoute(app);
|
||||
useKbRoute(app);
|
||||
useSystemRoute(app);
|
||||
|
||||
const userSchema = new mongoose.Schema({
|
||||
_id: mongoose.Schema.Types.ObjectId,
|
||||
username: String,
|
||||
password: String,
|
||||
balance: Number,
|
||||
promotion: {
|
||||
rate: Number,
|
||||
},
|
||||
openaiKey: String,
|
||||
avatar: String,
|
||||
createTime: Date,
|
||||
});
|
||||
|
||||
// 新增: 定义 pays 模型
|
||||
const paySchema = new mongoose.Schema({
|
||||
_id: mongoose.Schema.Types.ObjectId,
|
||||
userId: mongoose.Schema.Types.ObjectId,
|
||||
price: Number,
|
||||
orderId: String,
|
||||
status: String,
|
||||
createTime: Date,
|
||||
__v: Number,
|
||||
});
|
||||
|
||||
// 新增: 定义 kb 模型
|
||||
const kbSchema = new mongoose.Schema({
|
||||
_id: mongoose.Schema.Types.ObjectId,
|
||||
userId: mongoose.Schema.Types.ObjectId,
|
||||
avatar: String,
|
||||
name: String,
|
||||
tags: [String],
|
||||
updateTime: Date,
|
||||
__v: Number,
|
||||
});
|
||||
|
||||
|
||||
const modelSchema = new mongoose.Schema({
|
||||
userId: mongoose.Schema.Types.ObjectId,
|
||||
name: String,
|
||||
avatar: String,
|
||||
status: String,
|
||||
chat: {
|
||||
relatedKbs: [mongoose.Schema.Types.ObjectId],
|
||||
searchMode: String,
|
||||
systemPrompt: String,
|
||||
temperature: Number,
|
||||
chatModel: String
|
||||
},
|
||||
share: {
|
||||
isShare: Boolean,
|
||||
isShareDetail: Boolean,
|
||||
intro: String,
|
||||
collection: Number
|
||||
},
|
||||
security: {
|
||||
domain: [String],
|
||||
contextMaxLen: Number,
|
||||
contentMaxLen: Number,
|
||||
expiredTime: Number,
|
||||
maxLoadAmount: Number
|
||||
},
|
||||
updateTime: Date
|
||||
});
|
||||
|
||||
|
||||
const Model = mongoose.model('Model', modelSchema);
|
||||
const Kb = mongoose.model('Kb', kbSchema);
|
||||
const User = mongoose.model('User', userSchema, 'users');
|
||||
const Pay = mongoose.model('Pay', paySchema, 'pays');
|
||||
|
||||
// 获取用户列表
|
||||
app.get('/users', async (req, res) => {
|
||||
try {
|
||||
const start = parseInt(req.query._start) || 0;
|
||||
const end = parseInt(req.query._end) || 20;
|
||||
const order = req.query._order === 'DESC' ? -1 : 1;
|
||||
const sort = req.query._sort || '_id';
|
||||
|
||||
const usersRaw = await User.find()
|
||||
.skip(start)
|
||||
.limit(end - start)
|
||||
.sort({ [sort]: order });
|
||||
const users = usersRaw.map((user) => {
|
||||
const obj = user.toObject();
|
||||
obj.id = obj._id;
|
||||
delete obj._id;
|
||||
return obj;
|
||||
});
|
||||
|
||||
const totalCount = await User.countDocuments();
|
||||
|
||||
res.header('Access-Control-Expose-Headers', 'X-Total-Count');
|
||||
res.header('X-Total-Count', totalCount);
|
||||
res.json(users);
|
||||
} catch (err) {
|
||||
console.log(`Error fetching users: ${err}`);
|
||||
res.status(500).json({ error: 'Error fetching users' });
|
||||
}
|
||||
});
|
||||
|
||||
// 创建用户
|
||||
app.post('/users', async (req, res) => {
|
||||
try {
|
||||
const { username, password, balance, promotion, openaiKey = '', avatar = '/icon/human.png' } = req.body;
|
||||
if (!username || !password || !balance) {
|
||||
return res.status(400).json({ error: 'Invalid user information' });
|
||||
}
|
||||
const existingUser = await User.findOne({ username });
|
||||
if (existingUser) {
|
||||
return res.status(400).json({ error: 'Username already exists' });
|
||||
}
|
||||
const user = new User({
|
||||
_id: new mongoose.Types.ObjectId(),
|
||||
username,
|
||||
password,
|
||||
balance,
|
||||
promotion: {
|
||||
rate: promotion?.rate || 0,
|
||||
},
|
||||
openaiKey,
|
||||
avatar,
|
||||
createTime: new Date(),
|
||||
});
|
||||
const result = await user.save();
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
console.log(`Error creating user: ${err}`);
|
||||
res.status(500).json({ error: 'Error creating user' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 修改用户信息
|
||||
app.put('/users/:id', async (req, res) => {
|
||||
try {
|
||||
const _id = req.params.id;
|
||||
|
||||
const result = await User.updateOne({ _id: _id }, { $set: req.body });
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
console.log(`Error updating user: ${err}`);
|
||||
res.status(500).json({ error: 'Error updating user' });
|
||||
}
|
||||
});
|
||||
|
||||
// 删除用户
|
||||
app.delete('/users/:id', async (req, res) => {
|
||||
try {
|
||||
const _id = req.params.id;
|
||||
if (!mongoose.Types.ObjectId.isValid(_id)) {
|
||||
return res.status(400).json({ error: 'Invalid user ID' });
|
||||
}
|
||||
const result = await User.deleteOne({ _id: _id });
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
console.log(`Error deleting user: ${err}`);
|
||||
res.status(500).json({ error: 'Error deleting user' });
|
||||
}
|
||||
});
|
||||
|
||||
// 新增: 获取 pays 列表
|
||||
app.get('/pays', async (req, res) => {
|
||||
try {
|
||||
const start = parseInt(req.query._start) || 0;
|
||||
const end = parseInt(req.query._end) || 20;
|
||||
const order = req.query._order === 'DESC' ? -1 : 1;
|
||||
const sort = req.query._sort || '_id';
|
||||
|
||||
const paysRaw = await Pay.find()
|
||||
.skip(start)
|
||||
.limit(end - start)
|
||||
.sort({ [sort]: order });
|
||||
|
||||
const usersMap = new Map();
|
||||
const pays = [];
|
||||
|
||||
for (const payRaw of paysRaw) {
|
||||
const pay = payRaw.toObject();
|
||||
|
||||
if (!usersMap.has(pay.userId.toString())) {
|
||||
const user = await User.findById(pay.userId);
|
||||
usersMap.set(pay.userId.toString(), user.username);
|
||||
}
|
||||
|
||||
const orderedPay = {
|
||||
id: pay._id.toString(),
|
||||
name: usersMap.get(pay.userId.toString()),
|
||||
price: pay.price,
|
||||
orderId: pay.orderId,
|
||||
status: pay.status,
|
||||
createTime: pay.createTime
|
||||
};
|
||||
|
||||
pays.push(orderedPay);
|
||||
}
|
||||
const totalCount = await Pay.countDocuments();
|
||||
res.header('Access-Control-Expose-Headers', 'X-Total-Count');
|
||||
res.header('X-Total-Count', totalCount);
|
||||
res.json(pays);
|
||||
} catch (err) {
|
||||
console.log(`Error fetching pays: ${err}`);
|
||||
res.status(500).json({ error: 'Error fetching pays', details: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取用户知识库列表
|
||||
app.get('/kbs', async (req, res) => {
|
||||
try {
|
||||
const start = parseInt(req.query._start) || 0;
|
||||
const end = parseInt(req.query._end) || 20;
|
||||
const order = req.query._order === 'DESC' ? -1 : 1;
|
||||
const sort = req.query._sort || '_id';
|
||||
|
||||
const kbsRaw = await Kb.find()
|
||||
.skip(start)
|
||||
.limit(end - start)
|
||||
.sort({ [sort]: order });
|
||||
|
||||
const usersMap = new Map();
|
||||
const kbs = [];
|
||||
|
||||
for (const kbRaw of kbsRaw) {
|
||||
const kb = kbRaw.toObject();
|
||||
|
||||
if (!usersMap.has(kb.userId.toString())) {
|
||||
const user = await User.findById(kb.userId);
|
||||
usersMap.set(kb.userId.toString(), user.username);
|
||||
}
|
||||
|
||||
const orderedKb = {
|
||||
id: kb._id.toString(),
|
||||
user: usersMap.get(kb.userId.toString()),
|
||||
name: kb.name,
|
||||
tags: kb.tags,
|
||||
avatar: kb.avatar
|
||||
};
|
||||
|
||||
kbs.push(orderedKb);
|
||||
}
|
||||
const totalCount = await Kb.countDocuments();
|
||||
res.header('Access-Control-Expose-Headers', 'X-Total-Count');
|
||||
res.header('X-Total-Count', totalCount);
|
||||
res.json(kbs);
|
||||
} catch (err) {
|
||||
console.log(`Error fetching kbs: ${err}`);
|
||||
res.status(500).json({ error: 'Error fetching kbs', details: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取AI助手列表
|
||||
app.get('/models', async (req, res) => {
|
||||
try {
|
||||
const start = parseInt(req.query._start) || 0;
|
||||
const end = parseInt(req.query._end) || 20;
|
||||
const order = req.query._order === 'DESC' ? -1 : 1;
|
||||
const sort = req.query._sort || '_id';
|
||||
|
||||
const modelsRaw = await Model.find()
|
||||
.skip(start)
|
||||
.limit(end - start)
|
||||
.sort({ [sort]: order });
|
||||
|
||||
const usersMap = new Map();
|
||||
const models = [];
|
||||
|
||||
for (const modelRaw of modelsRaw) {
|
||||
const model = modelRaw.toObject();
|
||||
|
||||
if (!usersMap.has(model.userId.toString())) {
|
||||
const user = await User.findById(model.userId);
|
||||
usersMap.set(model.userId.toString(), user.username);
|
||||
}
|
||||
|
||||
// 获取与模型关联的知识库名称
|
||||
const kbNames = [];
|
||||
for (const kbId of model.chat.relatedKbs) {
|
||||
const kb = await Kb.findById(kbId);
|
||||
kbNames.push(kb.name);
|
||||
}
|
||||
|
||||
const orderedModel = {
|
||||
id: model._id.toString(),
|
||||
user: usersMap.get(model.userId.toString()),
|
||||
name: model.name,
|
||||
relatedKbs: kbNames, // 将relatedKbs的id转换为相应的Kb名称
|
||||
searchMode: model.chat.searchMode,
|
||||
systemPrompt: model.chat.systemPrompt,
|
||||
temperature: model.chat.temperature,
|
||||
isShare: model.share.isShare,
|
||||
isShareDetail: model.share.isShareDetail,
|
||||
avatar: model.avatar
|
||||
};
|
||||
|
||||
models.push(orderedModel);
|
||||
}
|
||||
const totalCount = await Model.countDocuments();
|
||||
res.header('Access-Control-Expose-Headers', 'X-Total-Count');
|
||||
res.header('X-Total-Count', totalCount);
|
||||
res.json(models);
|
||||
} catch (err) {
|
||||
console.log(`Error fetching models: ${err}`);
|
||||
res.status(500).json({ error: 'Error fetching models', details: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
const PORT = process.env.PORT || 3001;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server is running on port ${PORT}`);
|
||||
});
|
||||
|
||||
|
56
admin/service/route/app.js
Normal file
56
admin/service/route/app.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import { User, Model, Kb } from '../schema.js';
|
||||
|
||||
export const useAppRoute = (app) => {
|
||||
// 获取AI助手列表
|
||||
app.get('/models', async (req, res) => {
|
||||
try {
|
||||
const start = parseInt(req.query._start) || 0;
|
||||
const end = parseInt(req.query._end) || 20;
|
||||
const order = req.query._order === 'DESC' ? -1 : 1;
|
||||
const sort = req.query._sort || '_id';
|
||||
const userId = req.query.userId || '';
|
||||
const name = req.query.name || '';
|
||||
const where = {
|
||||
...(userId ? { userId: userId } : {}),
|
||||
name
|
||||
};
|
||||
|
||||
const modelsRaw = await Model.find()
|
||||
.skip(start)
|
||||
.limit(end - start)
|
||||
.sort({ [sort]: order });
|
||||
|
||||
const models = [];
|
||||
|
||||
for (const modelRaw of modelsRaw) {
|
||||
const model = modelRaw.toObject();
|
||||
|
||||
// 获取与模型关联的知识库名称
|
||||
const kbNames = [];
|
||||
for (const kbId of model.chat.relatedKbs) {
|
||||
const kb = await Kb.findById(kbId);
|
||||
kbNames.push(kb.name);
|
||||
}
|
||||
|
||||
const orderedModel = {
|
||||
id: model._id.toString(),
|
||||
userId: model.userId,
|
||||
name: model.name,
|
||||
relatedKbs: kbNames, // 将relatedKbs的id转换为相应的Kb名称
|
||||
searchMode: model.chat?.searchMode,
|
||||
systemPrompt: model.chat?.systemPrompt || '',
|
||||
temperature: model.chat?.temperature
|
||||
};
|
||||
|
||||
models.push(orderedModel);
|
||||
}
|
||||
const totalCount = await Model.countDocuments();
|
||||
res.header('Access-Control-Expose-Headers', 'X-Total-Count');
|
||||
res.header('X-Total-Count', totalCount);
|
||||
res.json(models);
|
||||
} catch (err) {
|
||||
console.log(`Error fetching models: ${err}`);
|
||||
res.status(500).json({ error: 'Error fetching models', details: err.message });
|
||||
}
|
||||
});
|
||||
};
|
43
admin/service/route/kb.js
Normal file
43
admin/service/route/kb.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Kb } from '../schema.js';
|
||||
|
||||
export const useKbRoute = (app) => {
|
||||
// 获取用户知识库列表
|
||||
app.get('/kbs', async (req, res) => {
|
||||
try {
|
||||
const start = parseInt(req.query._start) || 0;
|
||||
const end = parseInt(req.query._end) || 20;
|
||||
const order = req.query._order === 'DESC' ? -1 : 1;
|
||||
const sort = req.query._sort || '_id';
|
||||
const tag = req.query.tag || '';
|
||||
const where = { tags: { $elemMatch: { $regex: tag, $options: 'i' } } };
|
||||
|
||||
const kbsRaw = await Kb.find(where)
|
||||
.skip(start)
|
||||
.limit(end - start)
|
||||
.sort({ [sort]: order });
|
||||
|
||||
const kbs = [];
|
||||
|
||||
for (const kbRaw of kbsRaw) {
|
||||
const kb = kbRaw.toObject();
|
||||
|
||||
const orderedKb = {
|
||||
id: kb._id.toString(),
|
||||
userId: kb.userId,
|
||||
name: kb.name,
|
||||
tags: kb.tags,
|
||||
avatar: kb.avatar
|
||||
};
|
||||
|
||||
kbs.push(orderedKb);
|
||||
}
|
||||
const totalCount = await Kb.countDocuments(where);
|
||||
res.header('Access-Control-Expose-Headers', 'X-Total-Count');
|
||||
res.header('X-Total-Count', totalCount);
|
||||
res.json(kbs);
|
||||
} catch (err) {
|
||||
console.log(`Error fetching kbs: ${err}`);
|
||||
res.status(500).json({ error: 'Error fetching kbs', details: err.message });
|
||||
}
|
||||
});
|
||||
};
|
68
admin/service/route/system.js
Normal file
68
admin/service/route/system.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
const adminAuth = {
|
||||
username: process.env.ADMIN_USER,
|
||||
password: process.env.ADMIN_PASS
|
||||
};
|
||||
const authSecret = process.env.ADMIN_SECRET;
|
||||
|
||||
export const useSystemRoute = (app) => {
|
||||
app.post('/api/login', (req, res) => {
|
||||
if (!adminAuth.username || !adminAuth.password) {
|
||||
res.status(401).end('Server not set env: ADMIN_USER, ADMIN_PASS');
|
||||
return;
|
||||
}
|
||||
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (username === adminAuth.username && password === adminAuth.password) {
|
||||
// 用户名和密码都正确,返回token
|
||||
const token = jwt.sign(
|
||||
{
|
||||
username,
|
||||
platform: 'admin'
|
||||
},
|
||||
authSecret,
|
||||
{
|
||||
expiresIn: '2h'
|
||||
}
|
||||
);
|
||||
|
||||
res.json({
|
||||
username,
|
||||
token: token,
|
||||
expiredAt: new Date().valueOf() + 2 * 60 * 60 * 1000
|
||||
});
|
||||
} else {
|
||||
res.status(401).end('username or password incorrect');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const auth = () => {
|
||||
return (req, res, next) => {
|
||||
try {
|
||||
const authorization = req.headers.authorization;
|
||||
if (!authorization) {
|
||||
res.status(401).end('not found authorization in headers');
|
||||
return;
|
||||
}
|
||||
|
||||
const token = authorization.slice('Bearer '.length);
|
||||
|
||||
const payload = jwt.verify(token, authSecret);
|
||||
if (typeof payload === 'string') {
|
||||
res.status(401).end('payload type error');
|
||||
return;
|
||||
}
|
||||
if (payload.platform !== 'admin') {
|
||||
res.status(401).end('Payload invalid');
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (err) {
|
||||
res.status(401).end(String(err));
|
||||
}
|
||||
};
|
||||
};
|
138
admin/service/route/user.js
Normal file
138
admin/service/route/user.js
Normal file
@@ -0,0 +1,138 @@
|
||||
import { User, Pay } from '../schema.js';
|
||||
import dayjs from 'dayjs';
|
||||
import { auth } from './system.js';
|
||||
|
||||
export const useUserRoute = (app) => {
|
||||
// 获取用户列表
|
||||
app.get('/users', auth(), async (req, res) => {
|
||||
try {
|
||||
const start = parseInt(req.query._start) || 0;
|
||||
const end = parseInt(req.query._end) || 20;
|
||||
const order = req.query._order === 'DESC' ? -1 : 1;
|
||||
const sort = req.query._sort || 'createTime';
|
||||
const username = req.query.username || '';
|
||||
const where = {
|
||||
username: { $regex: username, $options: 'i' }
|
||||
};
|
||||
|
||||
const usersRaw = await User.find(where)
|
||||
.skip(start)
|
||||
.limit(end - start)
|
||||
.sort({ [sort]: order });
|
||||
|
||||
const users = usersRaw.map((user) => {
|
||||
const obj = user.toObject();
|
||||
return {
|
||||
...obj,
|
||||
id: obj._id,
|
||||
createTime: dayjs(obj.createTime).format('YYYY/MM/DD HH:mm')
|
||||
};
|
||||
});
|
||||
|
||||
const totalCount = await User.countDocuments(where);
|
||||
|
||||
res.header('Access-Control-Expose-Headers', 'X-Total-Count');
|
||||
res.header('X-Total-Count', totalCount);
|
||||
res.json(users);
|
||||
} catch (err) {
|
||||
console.log(`Error fetching users: ${err}`);
|
||||
res.status(500).json({ error: 'Error fetching users' });
|
||||
}
|
||||
});
|
||||
|
||||
// 创建用户
|
||||
app.post('/users', auth(), async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
username,
|
||||
password,
|
||||
balance,
|
||||
promotion,
|
||||
openaiKey = '',
|
||||
avatar = '/icon/human.png'
|
||||
} = req.body;
|
||||
if (!username || !password || !balance) {
|
||||
return res.status(400).json({ error: 'Invalid user information' });
|
||||
}
|
||||
const existingUser = await User.findOne({ username });
|
||||
if (existingUser) {
|
||||
return res.status(400).json({ error: 'Username already exists' });
|
||||
}
|
||||
const user = new User({
|
||||
_id: new mongoose.Types.ObjectId(),
|
||||
username,
|
||||
password,
|
||||
balance,
|
||||
promotion: {
|
||||
rate: promotion?.rate || 0
|
||||
},
|
||||
openaiKey,
|
||||
avatar,
|
||||
createTime: new Date()
|
||||
});
|
||||
const result = await user.save();
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
console.log(`Error creating user: ${err}`);
|
||||
res.status(500).json({ error: 'Error creating user' });
|
||||
}
|
||||
});
|
||||
|
||||
// 修改用户信息
|
||||
app.put('/users/:id', auth(), async (req, res) => {
|
||||
try {
|
||||
const _id = req.params.id;
|
||||
|
||||
const result = await User.updateOne({ _id: _id }, { $set: req.body });
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
console.log(`Error updating user: ${err}`);
|
||||
res.status(500).json({ error: 'Error updating user' });
|
||||
}
|
||||
});
|
||||
|
||||
// 新增: 获取 pays 列表
|
||||
app.get('/pays', auth(), async (req, res) => {
|
||||
try {
|
||||
const start = parseInt(req.query._start) || 0;
|
||||
const end = parseInt(req.query._end) || 20;
|
||||
const order = req.query._order === 'DESC' ? -1 : 1;
|
||||
const sort = req.query._sort || '_id';
|
||||
const userId = req.query.userId || '';
|
||||
const where = userId ? { userId: userId } : {};
|
||||
|
||||
const paysRaw = await Pay.find({
|
||||
...where
|
||||
})
|
||||
.skip(start)
|
||||
.limit(end - start)
|
||||
.sort({ [sort]: order });
|
||||
|
||||
const pays = [];
|
||||
|
||||
for (const payRaw of paysRaw) {
|
||||
const pay = payRaw.toObject();
|
||||
|
||||
const orderedPay = {
|
||||
id: pay._id.toString(),
|
||||
userId: pay.userId,
|
||||
price: pay.price,
|
||||
orderId: pay.orderId,
|
||||
status: pay.status,
|
||||
createTime: dayjs(pay.createTime).format('YYYY/MM/DD HH:mm')
|
||||
};
|
||||
|
||||
pays.push(orderedPay);
|
||||
}
|
||||
const totalCount = await Pay.countDocuments({
|
||||
...where
|
||||
});
|
||||
res.header('Access-Control-Expose-Headers', 'X-Total-Count');
|
||||
res.header('X-Total-Count', totalCount);
|
||||
res.json(pays);
|
||||
} catch (err) {
|
||||
console.log(`Error fetching pays: ${err}`);
|
||||
res.status(500).json({ error: 'Error fetching pays', details: err.message });
|
||||
}
|
||||
});
|
||||
};
|
90
admin/service/schema.js
Normal file
90
admin/service/schema.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import mongoose from 'mongoose';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config({ path: '.env.local' });
|
||||
|
||||
const mongoUrl = process.env.MONGODB_URI;
|
||||
const mongoDBName = process.env.MONGODB_NAME;
|
||||
|
||||
if (!mongoUrl || !mongoDBName) {
|
||||
throw new Error('db error');
|
||||
}
|
||||
|
||||
mongoose
|
||||
.connect(mongoUrl, {
|
||||
dbName: mongoDBName,
|
||||
bufferCommands: true,
|
||||
maxPoolSize: 5,
|
||||
minPoolSize: 1,
|
||||
maxConnecting: 5
|
||||
})
|
||||
.then(() => console.log('Connected to MongoDB successfully!'))
|
||||
.catch((err) => console.log(`Error connecting to MongoDB: ${err}`));
|
||||
|
||||
const userSchema = new mongoose.Schema({
|
||||
_id: mongoose.Schema.Types.ObjectId,
|
||||
username: String,
|
||||
password: String,
|
||||
balance: Number,
|
||||
promotion: {
|
||||
rate: Number
|
||||
},
|
||||
openaiKey: String,
|
||||
avatar: String,
|
||||
createTime: Date
|
||||
});
|
||||
|
||||
// 新增: 定义 pays 模型
|
||||
const paySchema = new mongoose.Schema({
|
||||
_id: mongoose.Schema.Types.ObjectId,
|
||||
userId: mongoose.Schema.Types.ObjectId,
|
||||
price: Number,
|
||||
orderId: String,
|
||||
status: String,
|
||||
createTime: Date,
|
||||
__v: Number
|
||||
});
|
||||
|
||||
// 新增: 定义 kb 模型
|
||||
const kbSchema = new mongoose.Schema({
|
||||
_id: mongoose.Schema.Types.ObjectId,
|
||||
userId: mongoose.Schema.Types.ObjectId,
|
||||
avatar: String,
|
||||
name: String,
|
||||
tags: [String],
|
||||
updateTime: Date,
|
||||
__v: Number
|
||||
});
|
||||
|
||||
const modelSchema = new mongoose.Schema({
|
||||
userId: mongoose.Schema.Types.ObjectId,
|
||||
name: String,
|
||||
avatar: String,
|
||||
status: String,
|
||||
chat: {
|
||||
relatedKbs: [mongoose.Schema.Types.ObjectId],
|
||||
searchMode: String,
|
||||
systemPrompt: String,
|
||||
temperature: Number,
|
||||
chatModel: String
|
||||
},
|
||||
share: {
|
||||
isShare: Boolean,
|
||||
isShareDetail: Boolean,
|
||||
intro: String,
|
||||
collection: Number
|
||||
},
|
||||
security: {
|
||||
domain: [String],
|
||||
contextMaxLen: Number,
|
||||
contentMaxLen: Number,
|
||||
expiredTime: Number,
|
||||
maxLoadAmount: Number
|
||||
},
|
||||
updateTime: Date
|
||||
});
|
||||
|
||||
export const Model = mongoose.model('Model', modelSchema);
|
||||
export const Kb = mongoose.model('Kb', kbSchema);
|
||||
export const User = mongoose.model('User', userSchema, 'users');
|
||||
export const Pay = mongoose.model('Pay', paySchema, 'pays');
|
@@ -4,18 +4,34 @@ import {
|
||||
ListTable,
|
||||
Resource,
|
||||
Tushan,
|
||||
fetchJSON
|
||||
} from 'tushan';
|
||||
import { authProvider } from './auth';
|
||||
import { userFields,payFields,kbFields,ModelFields } from './fields';
|
||||
import { userFields, payFields, kbFields, ModelFields } from './fields';
|
||||
|
||||
const dataProvider = jsonServerProvider('http://localhost:3001');
|
||||
const authStorageKey = 'tushan:auth';
|
||||
|
||||
const httpClient: typeof fetchJSON = (url, options = {}) => {
|
||||
try {
|
||||
if (!options.headers) {
|
||||
options.headers = new Headers({ Accept: 'application/json' });
|
||||
}
|
||||
const { token } = JSON.parse(window.localStorage.getItem(authStorageKey) ?? '{}');
|
||||
(options.headers as Headers).set('Authorization', `Bearer ${token}`);
|
||||
|
||||
return fetchJSON(url, options);
|
||||
} catch (err) {
|
||||
return Promise.reject();
|
||||
}
|
||||
};
|
||||
|
||||
const dataProvider = jsonServerProvider(import.meta.env.VITE_PUBLIC_SERVER_URL, httpClient);
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Tushan
|
||||
basename="/"
|
||||
header={'fastgpt-admin'}
|
||||
footer={'Build with stakeswky'}
|
||||
header={'FastGpt-Admin'}
|
||||
dataProvider={dataProvider}
|
||||
authProvider={authProvider}
|
||||
>
|
||||
@@ -25,12 +41,12 @@ function App() {
|
||||
list={
|
||||
<ListTable
|
||||
filter={[
|
||||
createTextField('q', {
|
||||
label: 'Query',
|
||||
}),
|
||||
createTextField('username', {
|
||||
label: 'username'
|
||||
})
|
||||
]}
|
||||
fields={userFields}
|
||||
action={{ create: true, detail: true, edit: true, delete: true }}
|
||||
action={{ create: true, detail: true, edit: true }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@@ -40,6 +56,11 @@ function App() {
|
||||
label="支付记录"
|
||||
list={
|
||||
<ListTable
|
||||
filter={[
|
||||
createTextField('userId', {
|
||||
label: 'userId'
|
||||
})
|
||||
]}
|
||||
fields={payFields}
|
||||
action={{ detail: true }}
|
||||
/>
|
||||
@@ -50,6 +71,11 @@ function App() {
|
||||
label="知识库"
|
||||
list={
|
||||
<ListTable
|
||||
filter={[
|
||||
createTextField('tag', {
|
||||
label: 'tag'
|
||||
})
|
||||
]}
|
||||
fields={kbFields}
|
||||
action={{ detail: true }}
|
||||
/>
|
||||
@@ -57,13 +83,8 @@ function App() {
|
||||
/>
|
||||
<Resource
|
||||
name="models"
|
||||
label="Ai模型"
|
||||
list={
|
||||
<ListTable
|
||||
fields={ModelFields}
|
||||
action={{ detail: true }}
|
||||
/>
|
||||
}
|
||||
label="应用"
|
||||
list={<ListTable fields={ModelFields} action={{ detail: true }} />}
|
||||
/>
|
||||
</Tushan>
|
||||
);
|
||||
|
@@ -1,33 +1,5 @@
|
||||
import { AuthProvider } from 'tushan';
|
||||
import { createAuthProvider, type AuthProvider } from 'tushan';
|
||||
|
||||
export const authProvider: AuthProvider = {
|
||||
login: ({ username, password }) => {
|
||||
if (username !== 'tushan' || password !== 'tushan') {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
localStorage.setItem('username', username);
|
||||
return Promise.resolve();
|
||||
},
|
||||
logout: () => {
|
||||
localStorage.removeItem('username');
|
||||
return Promise.resolve();
|
||||
},
|
||||
checkAuth: () =>
|
||||
localStorage.getItem('username') ? Promise.resolve() : Promise.reject(),
|
||||
checkError: (error) => {
|
||||
const status = error.status;
|
||||
if (status === 401 || status === 403) {
|
||||
localStorage.removeItem('username');
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
getIdentity: () =>
|
||||
Promise.resolve({
|
||||
id: '0',
|
||||
fullName: 'Admin',
|
||||
}),
|
||||
getPermissions: () => Promise.resolve(''),
|
||||
};
|
||||
export const authProvider: AuthProvider = createAuthProvider({
|
||||
loginUrl: `${import.meta.env.VITE_PUBLIC_SERVER_URL}api/login`
|
||||
});
|
||||
|
@@ -1,46 +1,39 @@
|
||||
import {
|
||||
createTextField,
|
||||
createUrlField,
|
||||
createNumberField,
|
||||
createAvatarField
|
||||
} from 'tushan';
|
||||
import { createTextField, createNumberField } from 'tushan';
|
||||
|
||||
export const userFields = [
|
||||
createTextField('id', { label: 'ID' }),
|
||||
createTextField('username', { label: '用户名', list: { sort: true } }),
|
||||
createTextField('password', { label: '密码(加密)' }),
|
||||
createNumberField('balance', { label: '余额' }),
|
||||
createTextField('openaiKey', { label: 'OpenAI Key' }),
|
||||
createTextField('createTime', { label: 'Create Time' }),
|
||||
createAvatarField('avatar', { label: 'Avatar' }),
|
||||
createTextField('username', { label: '用户名' }),
|
||||
createNumberField('balance', { label: '余额', list: { sort: true } }),
|
||||
createTextField('createTime', { label: 'Create Time', list: { sort: true } })
|
||||
];
|
||||
|
||||
export const payFields = [
|
||||
createTextField('id', { label: 'ID' }),
|
||||
createTextField('name', { label: '用户名', list: { sort: true } }),
|
||||
createTextField('userId', { label: '用户Id' }),
|
||||
createNumberField('price', { label: '支付金额' }),
|
||||
createTextField('orderId', { label: 'orderId' }),
|
||||
createTextField('status', { label: '状态' }),
|
||||
createTextField('createTime', { label: 'Create Time' }),
|
||||
createTextField('createTime', { label: 'Create Time', list: { sort: true } })
|
||||
];
|
||||
|
||||
export const kbFields = [
|
||||
createTextField('id', { label: 'ID' }),
|
||||
createTextField('user', { label: '所属用户' }),
|
||||
createTextField('name', { label: '知识库', list: { sort: true } }),
|
||||
createTextField('tags', { label: 'Tags' }),
|
||||
createAvatarField('avatar', { label: 'Avatar' }),
|
||||
createTextField('userId', { label: '所属用户' }),
|
||||
createTextField('name', { label: '知识库' }),
|
||||
createTextField('tags', { label: 'Tags' })
|
||||
];
|
||||
|
||||
export const ModelFields = [
|
||||
createTextField('id', { label: 'ID' }),
|
||||
createTextField('name', { label: 'Ai助手', list: { sort: true } }),
|
||||
createTextField('user', { label: '所属用户' }),
|
||||
createTextField('userId', { label: '所属用户' }),
|
||||
createTextField('name', { label: '名字' }),
|
||||
createTextField('relatedKbs', { label: '引用的知识库' }),
|
||||
createTextField('searchMode', { label: '搜索模式' }),
|
||||
createTextField('systemPrompt', { label: '提示词' }),
|
||||
createTextField('temperature', { label: '温度' }),
|
||||
createTextField('isShare', { label: '是否分享' }),
|
||||
createTextField('isShareDetail', { label: '分享详情' }),
|
||||
createAvatarField('avatar', { label: 'Avatar' }),
|
||||
];
|
||||
createTextField('systemPrompt', {
|
||||
label: '提示词',
|
||||
list: {
|
||||
width: 400
|
||||
}
|
||||
}),
|
||||
createTextField('temperature', { label: '温度' })
|
||||
];
|
||||
|
@@ -2,6 +2,4 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<App />
|
||||
);
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />);
|
||||
|
@@ -70,12 +70,14 @@ services:
|
||||
- PG_HOST=0.0.0.0
|
||||
- PG_PORT=8100
|
||||
# 和上方PG镜像对应.
|
||||
- PG_USER=fastgpt # POSTGRES_USER
|
||||
- PG_PASSWORD=1234 # POSTGRES_PASSWORD
|
||||
- PG_DB_NAME=fastgpt # POSTGRES_DB
|
||||
- PG_USER=fastgpt
|
||||
- PG_PASSWORD=1234
|
||||
- PG_DB_NAME=fastgpt
|
||||
# openai
|
||||
- OPENAIKEY=sk-xxxxx,sk-xxx # 对话用的key,多个key,逗号分开
|
||||
- OPENAI_TRAINING_KEY=sk-xxx,sk-xxxx # 训练用的key
|
||||
# 对话用的key,多个key,逗号分开
|
||||
- OPENAIKEY=sk-xxxxx,sk-xxx
|
||||
# 训练用的key
|
||||
- OPENAI_TRAINING_KEY=sk-xxx,sk-xxxx
|
||||
- GPT4KEY=sk-xxx
|
||||
- OPENAI_BASE_URL=https://api.openai.com/v1
|
||||
- OPENAI_BASE_URL_AUTH=可选的安全凭证
|
||||
|
@@ -13,7 +13,7 @@
|
||||
"prettier": "^2.8.7"
|
||||
},
|
||||
"lint-staged": {
|
||||
"./**/src/**/*.{ts,tsx,scss}": "npm run format"
|
||||
"./**/*.{ts,tsx,scss}": "npm run format"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
|
Reference in New Issue
Block a user