()(
12 | immer((set) => ({
13 | isLogin: false,
14 | userIdentity: { id: '', fullName: '' },
15 | setIsLogin: (isLogin: boolean) => {
16 | set({ isLogin });
17 | },
18 | }))
19 | );
20 |
--------------------------------------------------------------------------------
/packages/tushan/client/types.ts:
--------------------------------------------------------------------------------
1 | export type PromiseType> = P extends Promise
2 | ? T
3 | : never;
4 |
5 | export type FunctionReturningPromise = (...args: any[]) => Promise;
6 |
7 | interface TushanRenderResourceFuncProps {
8 | permissions?: any;
9 | }
10 |
11 | export type TushanChildren =
12 | | React.ReactNode
13 | | (({ permissions }: TushanRenderResourceFuncProps) => React.ReactNode);
14 |
--------------------------------------------------------------------------------
/packages/tushan/client/utils/common.ts:
--------------------------------------------------------------------------------
1 | import urlRegex from 'url-regex';
2 |
3 | /**
4 | * Determine whether it is an available url
5 | */
6 | export function isValidUrl(str: unknown): str is string {
7 | return typeof str === 'string' && urlRegex({ exact: true }).test(str);
8 | }
9 |
10 | export function removeDoubleSlashes(path: string) {
11 | return path.replace('//', '/');
12 | }
13 |
14 | export function normalizeText(input: any): string {
15 | return input != null && typeof input !== 'string'
16 | ? JSON.stringify(input)
17 | : input;
18 | }
19 |
--------------------------------------------------------------------------------
/packages/tushan/client/utils/context.ts:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from 'react';
2 |
3 | /**
4 | * Create React Context with factory
5 | */
6 | export function createContextFactory(
7 | defaultValue: Props,
8 | displayName: string
9 | ) {
10 | const Context = createContext(defaultValue);
11 | Context.displayName = displayName;
12 |
13 | return {
14 | Provider: Context.Provider,
15 | useContext: function (): Props {
16 | return useContext(Context);
17 | },
18 | };
19 | }
20 |
--------------------------------------------------------------------------------
/packages/tushan/client/utils/createSelector.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Create Selector for zustand store hooks
3 | *
4 | * @example
5 | * const { userIdentity } = useUserStore(createSelector('userIdentity'));
6 | */
7 | export function createSelector(...keys: K[]) {
8 | return (state: T) => {
9 | const out: Pick = {} as any;
10 | keys.forEach((key) => {
11 | out[key] = state[key];
12 | });
13 |
14 | return out;
15 | };
16 | }
17 |
--------------------------------------------------------------------------------
/packages/tushan/client/utils/event.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'eventemitter-strict';
2 |
3 | export interface TushanEventMap {
4 | refreshList: (resource: string) => void;
5 | }
6 |
7 | export const sharedEvent = new EventEmitter();
8 |
--------------------------------------------------------------------------------
/packages/tushan/client/utils/tree.ts:
--------------------------------------------------------------------------------
1 | type SimpleTreeNode = { children?: SimpleTreeNode[] };
2 |
3 | /**
4 | * DFS
5 | */
6 | export function findTreeNode(
7 | tree: T[],
8 | selector: (node: T) => boolean
9 | ): T | null {
10 | for (const item of tree) {
11 | if (selector(item)) {
12 | return item;
13 | }
14 |
15 | if (item.children && Array.isArray(item.children)) {
16 | const finded = findTreeNode(item.children as any, selector);
17 |
18 | if (finded) {
19 | return finded;
20 | }
21 | }
22 | }
23 |
24 | return null;
25 | }
26 |
--------------------------------------------------------------------------------
/packages/tushan/client/utils/validator/email.ts:
--------------------------------------------------------------------------------
1 | import type { FieldValidator } from './types';
2 |
3 | const emailRE = /^[a-zA-Z0-9_\-\.]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_\-]+)+$/;
4 |
5 | export const emailValidator: FieldValidator = (value, cb) => {
6 | if (typeof value === 'string') {
7 | if (emailRE.test(value)) {
8 | cb();
9 | } else {
10 | cb('Not a validate email');
11 | }
12 | } else {
13 | cb(`value type must be string, now: ${typeof value}`);
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/packages/tushan/client/utils/validator/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types';
2 | export * from './email';
3 | export * from './mobile';
4 | export * from './url';
5 | export * from './strong-password';
6 |
--------------------------------------------------------------------------------
/packages/tushan/client/utils/validator/mobile.ts:
--------------------------------------------------------------------------------
1 | import type { FieldValidator } from './types';
2 |
3 | const mobileRE =
4 | /^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$/;
5 |
6 | export const mobileValidator: FieldValidator = (value, cb) => {
7 | if (typeof value === 'string') {
8 | if (mobileRE.test(value)) {
9 | cb();
10 | } else {
11 | cb('Not a validate mobile number');
12 | }
13 | } else {
14 | cb(`value type must be string, now: ${typeof value}`);
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/packages/tushan/client/utils/validator/strong-password.ts:
--------------------------------------------------------------------------------
1 | import type { FieldValidator } from './types';
2 |
3 | const strongPasswordRE = /^^[a-zA-Z]\w{5,17}$$/;
4 |
5 | export const strongPasswordValidator: FieldValidator = (value, cb) => {
6 | if (typeof value === 'string') {
7 | if (strongPasswordRE.test(value)) {
8 | cb();
9 | } else {
10 | cb(
11 | 'Password must be start with a letter, the length is between 6 and 18, and it can only contain letters, numbers and underscores'
12 | );
13 | }
14 | } else {
15 | cb(`value type must be string, now: ${typeof value}`);
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/packages/tushan/client/utils/validator/types.ts:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from 'react';
2 |
3 | export type FieldValidator = (
4 | value: FieldValue | undefined,
5 | callback: (error?: ReactNode) => void
6 | ) => void;
7 |
--------------------------------------------------------------------------------
/packages/tushan/client/utils/validator/url.ts:
--------------------------------------------------------------------------------
1 | import type { FieldValidator } from './types';
2 |
3 | const urlRE =
4 | /^(https?|ftp|file):\/\/[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]$/;
5 |
6 | export const urlValidator: FieldValidator = (value, cb) => {
7 | if (typeof value === 'string') {
8 | if (urlRE.test(value)) {
9 | cb();
10 | } else {
11 | cb('Not a validate url');
12 | }
13 | } else {
14 | cb(`value type must be string, now: ${typeof value}`);
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/packages/tushan/icon.ts:
--------------------------------------------------------------------------------
1 | export * from './client/icon';
2 |
--------------------------------------------------------------------------------
/packages/tushan/index.ts:
--------------------------------------------------------------------------------
1 | export * from './client';
2 |
--------------------------------------------------------------------------------
/packages/tushan/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tushan",
3 | "version": "0.3.1",
4 | "description": "",
5 | "main": "./index.ts",
6 | "files": [
7 | "client",
8 | "server",
9 | "index.ts",
10 | "icon.ts",
11 | "chart.ts",
12 | "tsconfig.json",
13 | "README.md"
14 | ],
15 | "keywords": [
16 | "react-admin",
17 | "react",
18 | "admin",
19 | "adminjs",
20 | "admin.js"
21 | ],
22 | "author": "moonrailgun ",
23 | "license": "MIT",
24 | "devDependencies": {
25 | "cross-env": "^7.0.3",
26 | "nodemon": "^2.0.19",
27 | "prettier": "^2.7.1",
28 | "rimraf": "^3.0.2",
29 | "ts-node": "^10.9.1",
30 | "typescript": "^4.9.4"
31 | },
32 | "dependencies": {
33 | "@arco-design/web-react": "^2.49.1",
34 | "@tanstack/react-query": "^4.28.0",
35 | "@tanstack/react-query-devtools": "^4.28.0",
36 | "@types/node": "^18.15.11",
37 | "@types/react": "^18.0.15",
38 | "@types/react-dom": "^18.0.6",
39 | "@types/jsonexport": "^3.0.2",
40 | "@types/lodash-es": "^4.17.7",
41 | "@types/qs": "^6.9.7",
42 | "@types/react-helmet": "^6.1.6",
43 | "@types/styled-components": "^5.1.26",
44 | "axios": "^0.27.2",
45 | "clsx": "^1.2.1",
46 | "eventemitter-strict": "^1.0.1",
47 | "i18next": "^22.4.15",
48 | "i18next-browser-languagedetector": "^7.0.1",
49 | "immer": "^9.0.19",
50 | "jsonexport": "^3.2.0",
51 | "lodash-es": "^4.17.21",
52 | "postcss": "^8.4.17",
53 | "qs": "^6.11.1",
54 | "react": "^18.2.0",
55 | "react-dom": "^18.2.0",
56 | "react-helmet": "^6.1.0",
57 | "react-i18next": "^12.2.2",
58 | "react-is": "^18.2.0",
59 | "react-json-view": "^1.21.3",
60 | "react-router": "^6.3.0",
61 | "react-router-dom": "^6.3.0",
62 | "recharts": "^2.13.0-alpha.3",
63 | "styled-components": "^5.3.9",
64 | "tailwindcss": "^3.1.8",
65 | "url-regex": "^5.0.0",
66 | "zustand": "^4.3.6"
67 | },
68 | "scripts": {
69 | "check:type": "tsc --noEmit",
70 | "build": "pnpm build-cjs && pnpm build-esm",
71 | "build-cjs": "rimraf ./dist/cjs && tsc --outDir dist/cjs",
72 | "build-esm": "rimraf ./dist/esm && tsc --outDir dist/esm --module es2015"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/packages/tushan/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": [
4 | "DOM"
5 | ],
6 | "skipLibCheck": true,
7 | "jsx": "react",
8 | "target": "esnext",
9 | "esModuleInterop": true,
10 | "isolatedModules": true,
11 | "module": "ESNext",
12 | "moduleResolution": "node",
13 | "strict": true,
14 | // "verbatimModuleSyntax": true,
15 | "importsNotUsedAsValues": "error",
16 | "emitDecoratorMetadata": true,
17 | "experimentalDecorators": true,
18 | "resolveJsonModule": true
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - 'packages/*'
3 |
--------------------------------------------------------------------------------
/public/logo.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weizihua/fastgpt-admin/d3542baeb2dce40d6ec53a19d7da1e3a45228174/public/logo.ico
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import cors from 'cors';
3 | import {useUserRoute} from './service/route/user.js';
4 | import {useAppRoute} from './service/route/app.js';
5 | import {useKbRoute} from './service/route/kb.js';
6 | import {useSystemRoute} from './service/route/system.js';
7 | import {useDashboardRoute} from "./service/route/dashboard.js";
8 | import {logMiddleware} from "./service/middleware/common.js";
9 |
10 | const app = express();
11 | app.use(cors());
12 | app.use(express.json());
13 | app.use(express.static('dist'));
14 | app.use(logMiddleware);
15 |
16 | useUserRoute(app);
17 | useAppRoute(app);
18 | useKbRoute(app);
19 | useSystemRoute(app);
20 | useDashboardRoute(app);
21 |
22 | app.get('/*', (req, res) => {
23 | try {
24 | res.sendFile(new URL('dist/index.html', import.meta.url).pathname);
25 | } catch (error) {
26 | res.end();
27 | }
28 | });
29 |
30 | app.use((err, req, res, next) => {
31 | try {
32 | res.sendFile(new URL('dist/index.html', import.meta.url).pathname);
33 | } catch (error) {
34 | res.end();
35 | }
36 | });
37 |
38 | const PORT = process.env.PORT || 3001;
39 | app.listen(PORT, () => {
40 | console.log(`Server is running on port ${PORT}`);
41 | });
42 |
--------------------------------------------------------------------------------
/service/constant/constant.js:
--------------------------------------------------------------------------------
1 | export const TeamCollectionName = 'teams';
2 | export const TeamMemberCollectionName = 'team_members';
3 | export const TeamTagsCollectionName = 'team_tags';
4 |
5 | // 团队 - 成员角色
6 | const TeamMemberRoleEnum = {
7 | owner: 'owner',
8 | admin: 'admin',
9 | visitor: 'visitor'
10 | };
11 |
12 | export const TeamMemberRoleMap = {
13 | [TeamMemberRoleEnum.owner]: {
14 | value: TeamMemberRoleEnum.owner,
15 | label: 'user.team.role.Owner'
16 | },
17 | [TeamMemberRoleEnum.admin]: {
18 | value: TeamMemberRoleEnum.admin,
19 | label: 'user.team.role.Admin'
20 | },
21 | [TeamMemberRoleEnum.visitor]: {
22 | value: TeamMemberRoleEnum.visitor,
23 | label: 'user.team.role.Visitor'
24 | }
25 | };
26 |
27 | // 团队 - 成员状态
28 | const TeamMemberStatusEnum = {
29 | waiting: 'waiting',
30 | active: 'active',
31 | reject: 'reject',
32 | leave: 'leave'
33 | };
34 |
35 | export const TeamMemberStatusMap = {
36 | [TeamMemberStatusEnum.waiting]: {
37 | label: 'user.team.member.waiting',
38 | color: 'orange.600'
39 | },
40 | [TeamMemberStatusEnum.active]: {
41 | label: 'user.team.member.active',
42 | color: 'green.600'
43 | },
44 | [TeamMemberStatusEnum.reject]: {
45 | label: 'user.team.member.reject',
46 | color: 'red.600'
47 | },
48 | [TeamMemberStatusEnum.leave]: {
49 | label: 'user.team.member.leave',
50 | color: 'red.600'
51 | }
52 | };
53 |
54 | export const notLeaveStatus = { $ne: TeamMemberStatusEnum.leave };
55 |
56 |
57 |
58 | // 定义应用类型映射
59 | export const AppTypeEnum = {
60 | simple: 'simple',
61 | advanced: 'advanced'
62 | };
63 |
64 | export const AppTypeMap = {
65 | [AppTypeEnum.simple]: {
66 | label: 'simple'
67 | },
68 | [AppTypeEnum.advanced]: {
69 | label: 'advanced'
70 | }
71 | };
72 |
73 | // 默认的 Whisper 配置
74 | export const defaultWhisperConfig = {
75 | open: false,
76 | autoSend: false,
77 | autoTTSResponse: false
78 | };
79 |
80 | // 默认的问题指南文本配置
81 | export const defaultQuestionGuideTextConfig = {
82 | open: false,
83 | textList: [],
84 | customURL: ''
85 | };
86 |
87 |
88 | export const AuthUserTypeEnum = {
89 | token: 'token',
90 | root: 'root',
91 | apikey: 'apikey',
92 | outLink: 'outLink',
93 | teamDomain: 'teamDomain'
94 | };
95 |
96 | export const PermissionTypeEnum = {
97 | private: 'private',
98 | public: 'public'
99 | };
100 |
101 | // 权限类型映射
102 | export const PermissionTypeMap = {
103 | [PermissionTypeEnum.private]: {
104 | iconLight: 'support/permission/privateLight',
105 | label: 'permission.Private'
106 | },
107 | [PermissionTypeEnum.public]: {
108 | iconLight: 'support/permission/publicLight',
109 | label: 'permission.Public'
110 | }
111 | };
112 |
113 | export const ResourceTypeEnum = {
114 | team: 'team',
115 | app: 'app',
116 | dataset: 'dataset'
117 | };
118 |
119 |
120 |
--------------------------------------------------------------------------------
/service/constant/permisson.js:
--------------------------------------------------------------------------------
1 | export const FullPermission = 7;
2 | export const RootPermission = 15;
3 |
--------------------------------------------------------------------------------
/service/middleware/common.js:
--------------------------------------------------------------------------------
1 | import {User} from "../schema/index.js";
2 | import {logger} from "../utils/winston.js";
3 |
4 | export const validateUserCreation = async (req, res, next) => {
5 | const {username, password} = req.body;
6 |
7 | if (!username || !password) {
8 | return res.status(400).json({error: '需要用户名和密码'});
9 | }
10 |
11 | // 检查用户名是否已存在
12 | try {
13 | const existingUser = await User.findOne({username});
14 | if (existingUser) {
15 | console.log(`已存在的用户: ${existingUser}`);
16 | return res.status(400).json({error: '用户名已存在'});
17 | }
18 | next();
19 | } catch (err) {
20 | console.log(`检查用户名存在时出错: ${err}`);
21 | res.status(500).json({error: '服务器错误,无法检查用户名是否存在'});
22 | }
23 | };
24 |
25 | export const logMiddleware = (req, res, next) => {
26 | const start = Date.now();
27 | res.on('finish', () => {
28 | const duration = Date.now() - start;
29 | const logLevel = res.statusCode >= 400 ? 'error' : 'info';
30 | const message = `${req.method} ${req.url} - Status Code: ${res.statusCode} - ${duration}ms `;
31 | logger[logLevel](message);
32 | });
33 | next();
34 | };
35 |
36 |
37 |
--------------------------------------------------------------------------------
/service/route/app.js:
--------------------------------------------------------------------------------
1 | import {App} from '../schema/index.js';
2 | import {auth} from './system.js';
3 |
4 | export const useAppRoute = (app) => {
5 | // 获取AI助手列表
6 | app.get('/apps', auth(), async (req, res) => {
7 | try {
8 | const start = parseInt(req.query._start) || 0;
9 | const end = parseInt(req.query._end) || 20;
10 | const order = req.query._order === 'DESC' ? -1 : 1;
11 | const sort = req.query._sort;
12 | const name = req.query.name || '';
13 | const id = req.query.id || '';
14 |
15 | const where = {
16 | ...(name && {name: {$regex: name, $options: 'i'}}),
17 | ...(id && {_id: id})
18 | };
19 |
20 | const modelsRaw = await App.find(where)
21 | .skip(start)
22 | .limit(end - start)
23 | // .sort({ [sort]: order, 'share.isShare': -1, 'share.collection': -1 });
24 |
25 | const models = [];
26 | // console.log(modelsRaw.length);
27 | for (const modelRaw of modelsRaw) {
28 | const app = modelRaw.toObject();
29 |
30 | // 获取与模型关联的知识库名称
31 | // const kbNames = [];
32 | // for (const kbId of app.chat.relatedKbs) {
33 | // const kb = await DataSet.findById(kbId);
34 | // kbNames.push(kb.name);
35 | // }
36 |
37 | const orderedModel = {
38 | id: app._id.toString(),
39 | // userId: app.userId,
40 | name: app.name,
41 | intro: app.intro,
42 | // relatedKbs: kbNames, // 将relatedKbs的id转换为相应的Kb名称
43 | systemPrompt: app.chat?.systemPrompt || '',
44 | temperature: app.chat?.temperature || 0,
45 | 'share.topNum': app.share?.topNum || 0,
46 | 'share.isShare': app.share?.isShare || false,
47 | 'share.collection': app.share?.collection || 0
48 | };
49 |
50 | models.push(orderedModel);
51 | }
52 | const totalCount = await App.countDocuments(where);
53 | res.header('Access-Control-Expose-Headers', 'X-Total-Count');
54 | res.header('X-Total-Count', totalCount);
55 | res.json(models);
56 | } catch (err) {
57 | console.log(`Error fetching models: ${err}`);
58 | res.status(500).json({error: 'Error fetching models', details: err.message});
59 | }
60 | });
61 |
62 | // 修改 app 信息
63 | app.put('/apps/:id', auth(), async (req, res) => {
64 | try {
65 | const _id = req.params.id;
66 |
67 | let {
68 | share: {isShare, topNum},
69 | intro
70 | } = req.body;
71 |
72 | await App.findByIdAndUpdate(_id, {
73 | $set: {
74 | intro: intro,
75 | 'share.topNum': Number(topNum),
76 | 'share.isShare': isShare === 'true' || isShare === true
77 | }
78 | });
79 |
80 | res.json({});
81 | } catch (err) {
82 | console.log(`Error updating user: ${err}`);
83 | res.status(500).json({error: 'Error updating user'});
84 | }
85 | });
86 | };
87 |
--------------------------------------------------------------------------------
/service/route/dashboard.js:
--------------------------------------------------------------------------------
1 | import {App} from "../schema/index.js";
2 |
3 | export const useDashboardRoute = (app) => {
4 | // 获取应用数量
5 | app.get('/apps/count', async (req, res) => {
6 | try {
7 | const totalAppsCount = await App.countDocuments();
8 | res.header('Access-Control-Expose-Headers', 'X-Total-Count');
9 | res.header('X-Total-Count', totalAppsCount);
10 | res.json({totalApps: totalAppsCount});
11 | } catch (err) {
12 | console.log(`Error getting app count: ${err}`);
13 | res.status(500).json({error: 'Error getting app count', details: err.message});
14 | }
15 | });
16 | }
17 |
--------------------------------------------------------------------------------
/service/route/kb.js:
--------------------------------------------------------------------------------
1 | import { DataSet } from '../schema/index.js';
2 | import { auth } from './system.js';
3 |
4 | export const useKbRoute = (app) => {
5 | // 获取用户知识库列表
6 | app.get('/kbs', auth(), async (req, res) => {
7 | try {
8 | const start = parseInt(req.query._start) || 0;
9 | const end = parseInt(req.query._end) || 20;
10 | const order = req.query._order === 'DESC' ? -1 : 1;
11 | const sort = req.query._sort || '_id';
12 | const tag = req.query.tag || '';
13 | const name = req.query.name || '';
14 |
15 | const where = {
16 | ...(name
17 | ? {
18 | name: { $regex: name, $options: 'i' }
19 | }
20 | : {}),
21 | ...(tag
22 | ? {
23 | tags: { $elemMatch: { $regex: tag, $options: 'i' } }
24 | }
25 | : {})
26 | };
27 |
28 | const kbsRaw = await DataSet.find(where)
29 | .skip(start)
30 | .limit(end - start)
31 | .sort({ [sort]: order });
32 |
33 | const kbs = [];
34 |
35 | for (const kbRaw of kbsRaw) {
36 | const kb = kbRaw.toObject();
37 |
38 | const orderedKb = {
39 | id: kb._id.toString(),
40 | userId: kb.tmbId,
41 | name: kb.name,
42 | // tags: kb.tags,
43 | avatar: kb.avatar
44 | };
45 |
46 | kbs.push(orderedKb);
47 | }
48 | const totalCount = await DataSet.countDocuments(where);
49 | res.header('Access-Control-Expose-Headers', 'X-Total-Count');
50 | res.header('X-Total-Count', totalCount);
51 | res.json(kbs);
52 | } catch (err) {
53 | console.log(`Error fetching kbs: ${err}`);
54 | res.status(500).json({ error: 'Error fetching kbs', details: err.message });
55 | }
56 | });
57 | };
58 |
--------------------------------------------------------------------------------
/service/route/system.js:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken';
2 |
3 | const adminAuth = {
4 | username: process.env.ADMIN_USER,
5 | password: process.env.ADMIN_PASS
6 | };
7 | const authSecret = process.env.ADMIN_SECRET;
8 |
9 | const postParent = () => {
10 | fetch(`${process.env.PARENT_URL}/api/system/updateEnv`, {
11 | headers: {
12 | rootkey: process.env.PARENT_ROOT_KEY
13 | }
14 | });
15 | };
16 |
17 | export const useSystemRoute = (app) => {
18 | // 登录
19 | app.post('/api/login', (req, res) => {
20 | if (!adminAuth.username || !adminAuth.password) {
21 | res.status(401).end('Server not set env: ADMIN_USER, ADMIN_PASS');
22 | return;
23 | }
24 |
25 | const {username, password} = req.body;
26 |
27 | if (username === adminAuth.username && password === adminAuth.password) {
28 | // 用户名和密码都正确,返回token
29 | const token = jwt.sign(
30 | {
31 | username,
32 | platform: 'admin'
33 | },
34 | authSecret,
35 | {
36 | expiresIn: '2h'
37 | }
38 | );
39 |
40 | res.json({
41 | username,
42 | token: token,
43 | expiredAt: new Date().valueOf() + 2 * 60 * 60 * 1000
44 | });
45 | } else {
46 | res.status(401).end('username or password incorrect');
47 | }
48 | });
49 |
50 | };
51 |
52 | export const auth = () => {
53 | return (req, res, next) => {
54 | try {
55 | const authorization = req.headers.authorization;
56 | if (!authorization) {
57 | return next(new Error('unAuthorization'));
58 | }
59 |
60 | const token = authorization.slice('Bearer '.length);
61 |
62 | const payload = jwt.verify(token, authSecret);
63 | if (typeof payload === 'string') {
64 | res.status(401).end('payload type error');
65 | return;
66 | }
67 | if (payload.platform !== 'admin') {
68 | res.status(401).end('Payload invalid');
69 | return;
70 | }
71 |
72 | next();
73 | } catch (err) {
74 | res.status(401).end(String(err));
75 | }
76 | };
77 | };
78 |
--------------------------------------------------------------------------------
/service/schema/appSchema.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import {AppTypeMap, PermissionTypeMap, TeamCollectionName, TeamMemberCollectionName} from "../constant/constant.js";
3 |
4 | export const appSchema = new mongoose.Schema({
5 | teamId: {
6 | type: mongoose.Schema.Types.ObjectId,
7 | ref: TeamCollectionName,
8 | required: true
9 | },
10 | tmbId: {
11 | type: mongoose.Schema.Types.ObjectId,
12 | ref: TeamMemberCollectionName,
13 | required: true
14 | },
15 | name: {
16 | type: String,
17 | required: true
18 | },
19 | type: {
20 | type: String,
21 | default: 'advanced',
22 | enum: Object.keys(AppTypeMap)
23 | },
24 | version: {
25 | type: String,
26 | enum: ['v1', 'v2']
27 | },
28 | avatar: {
29 | type: String,
30 | default: '/icon/logo.ico'
31 | },
32 | intro: {
33 | type: String,
34 | default: ''
35 | },
36 | updateTime: {
37 | type: Date,
38 | default: () => new Date()
39 | },
40 |
41 | // tmp store
42 | modules: {
43 | type: Array,
44 | default: []
45 | },
46 | edges: {
47 | type: Array,
48 | default: []
49 | },
50 |
51 | scheduledTriggerConfig: {
52 | cronString: {
53 | type: String
54 | },
55 | timezone: {
56 | type: String
57 | },
58 | defaultPrompt: {
59 | type: String
60 | }
61 | },
62 | scheduledTriggerNextTime: {
63 | type: Date
64 | },
65 |
66 | inited: {
67 | type: Boolean
68 | },
69 | permission: {
70 | type: String,
71 | enum: Object.keys(PermissionTypeMap),
72 | default: 'private'
73 | },
74 | teamTags: {
75 | type: [String]
76 | }
77 | });
78 |
--------------------------------------------------------------------------------
/service/schema/datasetSchema.js:
--------------------------------------------------------------------------------
1 | // 新增: 定义 kb 模型
2 | import mongoose from "mongoose";
3 |
4 | export const datasetSchema = new mongoose.Schema({
5 | _id: mongoose.Schema.Types.ObjectId,
6 | tmbId: mongoose.Schema.Types.ObjectId,
7 | avatar: String,
8 | name: String,
9 | updateTime: Date,
10 | permission: String,
11 | __v: Number
12 | });
13 |
--------------------------------------------------------------------------------
/service/schema/index.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 | import dotenv from 'dotenv';
3 | import {appSchema} from "./appSchema.js";
4 | import {datasetSchema} from "./datasetSchema.js";
5 | import {UserSchema} from "./userSchema.js";
6 | import {paySchema} from "./paySchema.js";
7 | import {SystemSchema} from "./systemSchema.js";
8 | import {TeamMemberSchema} from "./teamMemberSchema.js";
9 | import {TeamSchema} from "./teamSchema.js";
10 |
11 | dotenv.config({path: '.env.local'});
12 |
13 | const mongoUrl = process.env.MONGODB_URI;
14 | const mongoDBName = process.env.MONGODB_NAME;
15 |
16 | if (!mongoUrl || !mongoDBName) {
17 | throw new Error('db error');
18 | }
19 |
20 | // 连接数据库
21 | mongoose
22 | .connect(mongoUrl, {
23 | dbName: mongoDBName,
24 | bufferCommands: true,
25 | maxPoolSize: 5,
26 | minPoolSize: 1,
27 | maxConnecting: 5
28 | })
29 | .then(() => console.log('成功连接MongoDB!'))
30 | .catch((err) => console.log(`连接到MongoDB时出错: ${err}`));
31 |
32 | // 表结构
33 | export const App = mongoose.models['apps'] || mongoose.model('apps', appSchema);
34 | export const DataSet = mongoose.models['dataset'] || mongoose.model('dataset', datasetSchema);
35 | export const User = mongoose.models['user'] || mongoose.model('user', UserSchema);
36 | export const Pay = mongoose.models['pay'] || mongoose.model('pay', paySchema);
37 | export const System = mongoose.models['system'] || mongoose.model('system', SystemSchema);
38 | export const Team = mongoose.models['teams'] || mongoose.model('teams', TeamSchema);
39 | export const TeamMember = mongoose.models['team_members'] || mongoose.model('team_members', TeamMemberSchema);
40 |
--------------------------------------------------------------------------------
/service/schema/paySchema.js:
--------------------------------------------------------------------------------
1 | // 新增: 定义 pays 模型
2 | import mongoose from "mongoose";
3 |
4 | export const paySchema = new mongoose.Schema({
5 | userId: mongoose.Schema.Types.ObjectId,
6 | price: Number,
7 | orderId: String,
8 | status: String,
9 | createTime: Date,
10 | __v: Number
11 | });
12 |
--------------------------------------------------------------------------------
/service/schema/systemSchema.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | export const SystemSchema = new mongoose.Schema({
4 | vectorMaxProcess: {
5 | type: Number,
6 | default: 10
7 | },
8 | qaMaxProcess: {
9 | type: Number,
10 | default: 10
11 | },
12 | pgIvfflatProbe: {
13 | type: Number,
14 | default: 10
15 | },
16 | sensitiveCheck: {
17 | type: Boolean,
18 | default: false
19 | }
20 | });
21 |
--------------------------------------------------------------------------------
/service/schema/teamMemberSchema.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import {TeamCollectionName, TeamMemberRoleMap, TeamMemberStatusMap} from "../constant/constant.js";
3 | import {userCollectionName} from "./userSchema.js";
4 |
5 | export const TeamMemberSchema = new mongoose.Schema({
6 | teamId: {
7 | type: mongoose.Types.ObjectId,
8 | ref: TeamCollectionName,
9 | required: true
10 | },
11 | userId: {
12 | type: mongoose.Types.ObjectId,
13 | ref: userCollectionName,
14 | required: true
15 | },
16 | name: {
17 | type: String,
18 | default: 'Member'
19 | },
20 | role: {
21 | type: String,
22 | enum: Object.keys(TeamMemberRoleMap)
23 | },
24 | status: {
25 | type: String,
26 | enum: Object.keys(TeamMemberStatusMap)
27 | },
28 | createTime: {
29 | type: Date,
30 | default: () => new Date()
31 | },
32 | defaultTeam: {
33 | type: Boolean,
34 | default: false
35 | }
36 | });
37 |
--------------------------------------------------------------------------------
/service/schema/teamSchema.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import {userCollectionName} from "./userSchema.js";
3 | import {FullPermission} from "../constant/permisson.js";
4 |
5 | export const TeamSchema = new mongoose.Schema({
6 | name: {
7 | type: String,
8 | required: true
9 | },
10 | ownerId: {
11 | type: mongoose.Schema.Types.ObjectId,
12 | ref: userCollectionName
13 | },
14 | defaultPermission: {
15 | type: Number,
16 | default: FullPermission
17 | },
18 | avatar: {
19 | type: String,
20 | default: '/icon/logo.ico'
21 | },
22 | createTime: {
23 | type: Date,
24 | default: () => Date.now()
25 | },
26 | balance: {
27 | type: Number,
28 | default: 0
29 | },
30 | teamDomain: {
31 | type: String
32 | },
33 | limit: {
34 | lastExportDatasetTime: {
35 | type: Date
36 | },
37 | lastWebsiteSyncTime: {
38 | type: Date
39 | }
40 | },
41 | lafAccount: {
42 | token: {
43 | type: String
44 | },
45 | appid: {
46 | type: String
47 | },
48 | pat: {
49 | type: String
50 | }
51 | }
52 | });
53 |
54 | try {
55 | TeamSchema.index({name: 1});
56 | TeamSchema.index({ownerId: 1});
57 | } catch (error) {
58 | console.log(error);
59 | }
60 |
61 |
--------------------------------------------------------------------------------
/service/schema/userSchema.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import {hashPassword} from "../route/user.js";
3 |
4 | export const userCollectionName = 'users';
5 |
6 | export const UserSchema = new mongoose.Schema({
7 | username: {
8 | type: String,
9 | required: true,
10 | unique: true // 唯一
11 | },
12 | email: {
13 | type: String
14 | },
15 | phonePrefix: {
16 | type: Number
17 | },
18 | phone: {
19 | type: String
20 | },
21 | password: {
22 | type: String,
23 | required: true,
24 | set: (val) => hashPassword(val),
25 | get: (val) => hashPassword(val),
26 | select: false
27 | },
28 | createTime: {
29 | type: Date,
30 | default: () => new Date()
31 | },
32 | avatar: {
33 | type: String,
34 | default: '/icon/human.svg'
35 | },
36 | status: {
37 | type: String,
38 | default: 'active'
39 | },
40 | promotionRate: {
41 | type: Number,
42 | default: 15
43 | },
44 | openaiAccount: {
45 | type: {
46 | key: String,
47 | baseUrl: String
48 | }
49 | },
50 | timezone: {
51 | type: String,
52 | default: 'Asia/Shanghai'
53 | },
54 | lastLoginTmbId: {
55 | type: mongoose.Types.ObjectId
56 | }
57 | });
58 |
--------------------------------------------------------------------------------
/service/utils/index.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weizihua/fastgpt-admin/d3542baeb2dce40d6ec53a19d7da1e3a45228174/service/utils/index.js
--------------------------------------------------------------------------------
/service/utils/winston.js:
--------------------------------------------------------------------------------
1 | import winston from 'winston';
2 | import DailyRotateFile from 'winston-daily-rotate-file';
3 | import fs from 'fs';
4 | import path from 'path';
5 |
6 | function ensureLogDirectory() {
7 | const logDirectory = path.join(process.cwd(), 'logs');
8 | if (!fs.existsSync(logDirectory)) {
9 | fs.mkdirSync(logDirectory, {recursive: true});
10 | }
11 | return logDirectory;
12 | }
13 |
14 | // 创建日志文件传输配置
15 | function createFileTransport(logDirectory) {
16 | return new DailyRotateFile({
17 | filename: path.join(logDirectory, 'admin-%DATE%.log'),
18 | datePattern: 'YYYY-MM-DD',
19 | zippedArchive: true,
20 | maxSize: '20m',
21 | maxFiles: '14d',
22 | format: winston.format.combine(
23 | winston.format.timestamp({format: 'YYYY-MM-DD HH:mm:ss'}),
24 | winston.format.printf(info => `${info.timestamp} ${info.level}: ${info.message}`)
25 | ),
26 | });
27 | }
28 |
29 | // 创建控制台传输配置
30 | function createConsoleTransport() {
31 | return new winston.transports.Console({
32 | level: 'debug',
33 | format: winston.format.combine(
34 | winston.format.simple()
35 | )
36 | });
37 | }
38 |
39 | const logDirectory = ensureLogDirectory();
40 | const transport = createFileTransport(logDirectory);
41 | const consoleTransport = createConsoleTransport();
42 |
43 | // 初始化并导出logger
44 | export const logger = winston.createLogger({
45 | transports: [
46 | transport,
47 | consoleTransport
48 | ]
49 | });
50 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | createTextField,
3 | fetchJSON,
4 | HTTPClient,
5 | jsonServerProvider,
6 | ListTable,
7 | Resource,
8 | Tushan,
9 | TushanContextProps
10 | } from 'tushan';
11 | import { authProvider } from './auth';
12 | import { AppFields, kbFields, userFields } from './fields';
13 | import { Dashboard } from './Dashboard';
14 | import { IconApps, IconBook, IconUser } from 'tushan/icon';
15 | import { i18nZhTranslation } from 'tushan/client/i18n/resources/zh';
16 |
17 | const authStorageKey = 'tushan:auth';
18 |
19 | const httpClient: HTTPClient = (url, options = {}) => {
20 | try {
21 | if (!options.headers) {
22 | options.headers = new Headers({ Accept: 'application/json' });
23 | }
24 | const { token } = JSON.parse(window.localStorage.getItem(authStorageKey) ?? '{}');
25 | (options.headers as Headers).set('Authorization', `Bearer ${ token }`);
26 |
27 | return fetchJSON(url, options);
28 | } catch (err) {
29 | return Promise.reject();
30 | }
31 | };
32 |
33 | const dataProvider = jsonServerProvider(import.meta.env.VITE_PUBLIC_SERVER_URL, httpClient);
34 |
35 | const i18n: TushanContextProps['i18n'] = {
36 | languages: [
37 | {
38 | key: 'zh',
39 | label: '简体中文',
40 | translation: i18nZhTranslation
41 | }
42 | ]
43 | };
44 |
45 | function App() {
46 | return (
47 | }
54 | >
55 | }
59 | list={
60 |
69 | }
70 | />
71 | }
74 | label="应用"
75 | list={
76 |
88 | }
89 | />
90 | {/* }*/ }
94 | {/* list={*/ }
95 | {/* */ }
104 | {/* }*/ }
105 | {/*/>*/ }
106 | }
110 | list={
111 |
123 | }
124 | />
125 |
126 | );
127 | }
128 |
129 | export default App;
130 |
--------------------------------------------------------------------------------
/src/auth.ts:
--------------------------------------------------------------------------------
1 | import { type AuthProvider, createAuthProvider } from 'tushan';
2 |
3 | export const authProvider: AuthProvider = createAuthProvider({
4 | loginUrl: `${ import.meta.env.VITE_PUBLIC_SERVER_URL }/api/login`
5 | });
6 |
--------------------------------------------------------------------------------
/src/fields.ts:
--------------------------------------------------------------------------------
1 | import { createNumberField, createTextField } from 'tushan';
2 |
3 | export const userFields = [
4 | createTextField('username', { label: '用户名' }),
5 | createTextField('id', { label: 'ID' }),
6 | // createNumberField('balance', { label: '余额(元)', list: { sort: true } }),
7 | createTextField('createTime', {
8 | label: '创建时间',
9 | list: { sort: true },
10 | edit: { hidden: true }
11 | }),
12 | // createTextField('avatar', { label: '头像' }),
13 | // createTextField('status', { label: '状态' }),
14 | // createTextField('promotionRate', { label: '权 限', }),
15 | createTextField('password', { label: '密码', list: { hidden: true } }),
16 | // createTextField('lastLoginTmbId', { label: '最后登录团队ID', })
17 | ];
18 |
19 | export const payFields = [
20 | createTextField('id', { label: 'ID' }),
21 | createTextField('userId', { label: '用户Id' }),
22 | createNumberField('price', { label: '支付金额' }),
23 | createTextField('orderId', { label: 'orderId' }),
24 | createTextField('status', { label: '状态' }),
25 | createTextField('createTime', { label: '创建时间', list: { sort: true } })
26 | ];
27 |
28 | export const kbFields = [
29 | createTextField('id', { label: 'ID' }),
30 | createTextField('userId', { label: '所属用户', edit: { hidden: true } }),
31 | createTextField('name', { label: '知识库' }),
32 | createTextField('tags', { label: 'Tags' })
33 | ];
34 |
35 | export const AppFields = [
36 | createTextField('id', { label: 'ID' }),
37 | createTextField('userId', { label: '所属用户', list: { hidden: true }, edit: { hidden: true } }),
38 | createTextField('name', { label: '名字' }),
39 | createTextField('app', { label: '应用', edit: { hidden: true } }),
40 | createTextField('share.collection', { label: '收藏数', list: { sort: true } }),
41 | createTextField('share.topNum', { label: '置顶等级', list: { sort: true } }),
42 | createTextField('share.isShare', { label: '是否分享(true,false)' }),
43 | createTextField('intro', { label: '介绍', list: { width: 400 } }),
44 | createTextField('relatedKbs', { label: '引用的知识库', list: { hidden: true } }),
45 | createTextField('temperature', { label: '温度' }),
46 | createTextField('systemPrompt', {
47 | label: '提示词',
48 | list: {
49 | width: 400,
50 | hidden: true
51 | }
52 | })
53 | ];
54 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import App from './App';
4 |
5 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render();
6 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // "baseUrl": ".",
4 | // "paths": {
5 | // "@tushan/*": [
6 | // "./tushan/*"
7 | // ]
8 | // },
9 | "target": "ESNext",
10 | "useDefineForClassFields": true,
11 | "lib": [
12 | "DOM",
13 | "DOM.Iterable",
14 | "ESNext"
15 | ],
16 | "allowJs": false,
17 | "skipLibCheck": true,
18 | "esModuleInterop": false,
19 | "allowSyntheticDefaultImports": true,
20 | "strict": true,
21 | "forceConsistentCasingInFileNames": true,
22 | "module": "ESNext",
23 | "moduleResolution": "Node",
24 | "resolveJsonModule": true,
25 | "isolatedModules": true,
26 | "noEmit": true,
27 | "jsx": "react-jsx"
28 | },
29 | "include": [
30 | "src"
31 | ],
32 | "references": [
33 | {
34 | "path": "./tsconfig.node.json"
35 | }
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": ["vite.config.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 | import path from 'path';
4 |
5 | // https://vitejs.dev/config/
6 | export default defineConfig({
7 | plugins: [react()],
8 | // resolve: {
9 | // alias: {
10 | // '@tushan': path.resolve(__dirname, './tushan')
11 | // }
12 | // }
13 | });
14 |
--------------------------------------------------------------------------------