diff --git a/.gitignore b/.gitignore index f6a7e55..9589fba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ v-env .env !.env.example +.idea node_modules __pycache__ diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 03d9549..3086825 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -1,6 +1,69 @@ - \ No newline at end of file + diff --git a/server/endpoints/dept.js b/server/endpoints/dept.js new file mode 100644 index 0000000..2de0e74 --- /dev/null +++ b/server/endpoints/dept.js @@ -0,0 +1,160 @@ +const { Dept } = require("../models/dept"); +const { validatedRequest } = require("../utils/middleware/validatedRequest"); +const { + strictMultiUserRoleValid, + ROLES +} = require("../utils/middleware/multiUserProtected"); +function deptEndpoints(app) { + if (!app) return; + + app.get( + "/dept/list", + [validatedRequest, strictMultiUserRoleValid([ROLES.admin])], + async (_request, response) => { + try { + const depts = await Dept.where(); + response.status(200).json({ depts }); + } catch (e) { + console.error(e); + response.sendStatus(500).end(); + } + } + ); + + app.post("/dept/add", + [validatedRequest, strictMultiUserRoleValid([ROLES.admin])], + async (request, response) => { + try { + const dept = request.body; // 获取请求体中的部门数据 + // 检查部门名称是否唯一 + const isUnique = await Dept.checkDeptNameUnique(dept); + if (!isUnique) { + return response.status(400).json({ + success: false, + message: `新增部门 '${dept.deptName}' 失败,部门名称已存在`, + }); + }; + // 按照deptId查询父部门 + const parentDept = await Dept.get({ deptId: dept.parentId }); + dept.ancestors = parentDept.dept.ancestors + ',' + dept.parentId; + // 插入部门数据 + const insertedDept = await Dept.insertDept(dept); + // 返回成功响应 + response.status(200).json({ + success: true, + data: insertedDept, + }); + } catch (error) { + // 处理错误 + console.error("添加部门失败:", error); + response.status(500).json({ + success: false, + message: "添加部门失败,服务器内部错误", + }); + } + }); + + app.post("/dept/edit", + [validatedRequest, strictMultiUserRoleValid([ROLES.admin])], + async (request, response) => { + try { + const dept = request.body; // 获取请求体中的部门数据 + + // 检查部门名称是否唯一 + const isUnique = await Dept.checkDeptNameUnique(dept); + if (!isUnique) { + return response.status(400).json({ + success: false, + message: `修改部门 '${dept.deptName}' 失败,部门名称已存在`, + }); + } + + // 检查上级部门是否是自己 + if (dept.parentId === dept.deptId) { + return response.status(400).json({ + success: false, + message: `修改部门 '${dept.deptName}' 失败,上级部门不能是自己`, + }); + } + + // 检查部门是否包含未停用的子部门 + if (dept.status === 1) { + const normalChildrenCount = await Dept.selectNormalChildrenDeptById(dept.deptId); + if (normalChildrenCount > 0) { + return response.status(400).json({ + success: false, + message: "该部门包含未停用的子部门!", + }); + } + } + // 更新部门数据 + const updatedDept = await Dept.update(dept); + // 返回成功响应 + response.status(200).json({ + success: true, + data: updatedDept, + }); + } catch (error) { + // 处理错误 + console.error("修改部门失败:", error); + response.status(500).json({ + success: false, + message: "修改部门失败,服务器内部错误", + }); + } + }); + + // 删除部门的接口 + app.delete("/dept/:deptId", + [validatedRequest, strictMultiUserRoleValid([ROLES.admin])], + async (request, response) => { + try { + const deptId = parseInt(request.params.deptId); // 获取部门 ID + + // 检查部门是否有子部门 + const hasChild = await Dept.hasChildByDeptId(deptId); + if (hasChild) { + return response.status(400).json({ + success: false, + message: "存在下级部门,不允许删除", + }); + } + + // 检查部门是否存在用户 + const hasUser = await Dept.checkDeptExistUser(deptId); + if (hasUser) { + return response.status(400).json({ + success: false, + message: "部门存在用户,不允许删除", + }); + } + + // // 检查部门数据权限 + // const hasDataScope = await Dept.checkDeptDataScope(deptId); + // if (!hasDataScope) { + // return response.status(403).json({ + // success: false, + // message: "无权限删除该部门", + // }); + // } + + // 删除部门 + const deletedDept = await Dept.deleteDeptById(deptId); + + // 返回成功响应 + response.status(200).json({ + success: true, + data: deletedDept, + }); + } catch (error) { + // 处理错误 + console.error("删除部门失败:", error); + response.status(500).json({ + success: false, + message: "删除部门失败,服务器内部错误", + }); + } + }); +} + +module.exports = { deptEndpoints }; diff --git a/server/index.js b/server/index.js index 78964e7..85647d9 100644 --- a/server/index.js +++ b/server/index.js @@ -27,6 +27,7 @@ const { experimentalEndpoints } = require("./endpoints/experimental"); const { browserExtensionEndpoints } = require("./endpoints/browserExtension"); const { communityHubEndpoints } = require("./endpoints/communityHub"); const { agentFlowEndpoints } = require("./endpoints/agentFlows"); +const { deptEndpoints } = require("./endpoints/dept"); const app = express(); const apiRouter = express.Router(); const FILE_LIMIT = "3GB"; @@ -51,6 +52,7 @@ app.use("/api", apiRouter); systemEndpoints(apiRouter); extensionEndpoints(apiRouter); workspaceEndpoints(apiRouter); +deptEndpoints(apiRouter); workspaceThreadEndpoints(apiRouter); chatEndpoints(apiRouter); adminEndpoints(apiRouter); diff --git a/server/models/dept.js b/server/models/dept.js new file mode 100644 index 0000000..dd9cfc3 --- /dev/null +++ b/server/models/dept.js @@ -0,0 +1,238 @@ +const prisma = require("../utils/prisma"); + +/** + * @typedef {Object} Dept + * @property {number} deptId + * @property {number} parentId + * @property {string} ancestors + * @property {string} deptName + * @property {number} orderNum + * @property {number} status + * @property {number} delFlag + * @property {Date} createdAt + * @property {Date} lastUpdatedAt + */ + +const Dept = { + writable: [ + 'parentId', 'ancestors', 'deptName', 'orderNum', 'status', 'delFlag' + ], + validations: { + deptName: (newValue = '') => { + if (typeof newValue !== 'string' || newValue.length > 255) { + throw new Error('Dept name must be a string and cannot be longer than 255 characters'); + } + return newValue; + }, + orderNum: (newValue = 0) => { + const num = Number(newValue); + if (isNaN(num)) { + throw new Error('Order num must be a number'); + } + return num; + }, + status: (newValue = 0) => { + const status = Number(newValue); + if (isNaN(status) || status < 0 || status > 1) { + throw new Error('Status must be a number between 0 and 1'); + } + return status; + }, + delFlag: (newValue = 0) => { + const flag = Number(newValue); + if (isNaN(flag) || flag < 0 || flag > 1) { + throw new Error('Del flag must be a number between 0 and 1'); + } + return flag; + } + }, + castColumnValue: function (key, value) { + switch (key) { + case 'status': + case 'delFlag': + return Number(value); + default: + return value; + } + }, + create: async function ({ parentId, ancestors, deptName, orderNum, status = 0, delFlag = 0 }) { + try { + const validatedDeptName = this.validations.deptName(deptName); + const validatedOrderNum = this.validations.orderNum(orderNum); + const validatedStatus = this.validations.status(status); + const validatedDelFlag = this.validations.delFlag(delFlag); + + const dept = await prisma.dept.create({ + data: { + parentId, + ancestors, + deptName: validatedDeptName, + orderNum: validatedOrderNum, + status: validatedStatus, + delFlag: validatedDelFlag, + createdAt: new Date(), + lastUpdatedAt: new Date() + } + }); + + return { dept, error: null }; + } catch (error) { + console.error('FAILED TO CREATE DEPT.', error.message); + return { dept: null, error: error.message }; + } + }, + // 插入部门数据 + insertDept: async function (dept) { + try { + const insertedDept = await prisma.dept.create({ + data: { + deptName: dept.deptName, + parentId: dept.parentId || null, + ancestors: dept.ancestors || null, + orderNum: dept.orderNum || 0, + status: dept.status || 0, + delFlag: dept.delFlag || 0, + createdAt: new Date(), + lastUpdatedAt: new Date(), + }, + }); + return insertedDept; + } catch (error) { + console.error("插入部门数据失败:", error); + throw error; + } + }, + update: async function (deptId, updates = {}) { + try { + if (!deptId) throw new Error('No dept id provided for update'); + + const currentDept = await prisma.dept.findUnique({ where: { deptId } }); + if (!currentDept) throw new Error('Dept not found'); + + Object.entries(updates).forEach(([key, value]) => { + if (this.writable.includes(key)) { + if (Object.prototype.hasOwnProperty.call(this.validations, key)) { + updates[key] = this.validations[key](this.castColumnValue(key, value)); + } else { + updates[key] = this.castColumnValue(key, value); + } + } + }); + + updates.lastUpdatedAt = new Date(); + + const updatedDept = await prisma.dept.update({ where: { deptId }, data: updates }); + + return { success: true, error: null, dept: updatedDept }; + } catch (error) { + console.error(error.message); + return { success: false, error: error.message, dept: null }; + } + }, + get: async function (clause = {}) { + try { + const dept = await prisma.dept.findFirst({ where: clause, select: { + deptId: true, parentId: true, ancestors: true, deptName: true, orderNum: true, status: true, delFlag: true, createdAt: true, lastUpdatedAt: true + } }); + return dept ? { dept } : null; + } catch (error) { + console.error(error.message); + return null; + } + }, + delete: async function (clause = {}) { + try { + const affectedRows = await prisma.dept.deleteMany({ where: clause }); + return affectedRows > 0; + } catch (error) { + console.error(error.message); + return false; + } + }, + where: async function (clause = {}, limit = null) { + try { + const depts = await prisma.dept.findMany({ + where: clause, + take: limit !== null ? limit : undefined, + select: { + deptId: true, parentId: true, ancestors: true, deptName: true, orderNum: true, status: true, delFlag: true, createdAt: true, lastUpdatedAt: true + } + }); + return depts; + } catch (error) { + console.error(error.message); + return []; + } + }, + checkDeptNameUnique: async function (dept){ + try { + const existingDept = await prisma.dept.findFirst({ + where: { + deptName: dept.deptName, // 根据部门名称查询 + parentId: dept.parentId, // 排除父id + }, + }); + + // 如果查询到记录,说明部门名称已存在 + return !existingDept; + } catch (error) { + console.error('检查部门名称唯一性失败:', error); + throw error; + } + }, + + // 检查部门是否包含未停用的子部门 + selectNormalChildrenDeptById: async function (deptId) { + try { + // 查询所有祖先部门中包含当前部门的未停用部门 + const childrenDepts = await prisma.$queryRaw` + SELECT COUNT(*) as count + FROM sys_dept + WHERE status = 0 + AND del_flag = '0' + AND FIND_IN_SET(${deptId}, ancestors) + `; + + // 返回未停用的子部门数量 + return childrenDepts[0].count; + } catch (error) { + console.error("查询子部门失败:", error); + throw error; + } + }, + // 检查部门是否有子部门 + hasChildByDeptId: async function (deptId) { + try { + const children = await prisma.dept.findMany({ + where: { + parentId: deptId, // 查询当前部门的子部门 + delFlag: 0, + }, + }); + + // 如果有子部门,返回 true + return children.length > 0; + } catch (error) { + console.error("检查子部门失败:", error); + throw error; + } + }, + // 检查部门是否存在用户 + checkDeptExistUser: async function (deptId) { + try { + const users = await prisma.user.findMany({ + where: { + deptId: deptId, // 查询当前部门的用户 + }, + }); + + // 如果存在用户,返回 true + return users.length > 0; + } catch (error) { + console.error("检查部门用户失败:", error); + throw error; + } + }, +}; + +module.exports = { Dept }; diff --git a/server/prisma/migrations/20250222043007_init/migration.sql b/server/prisma/migrations/20250222043007_init/migration.sql new file mode 100644 index 0000000..635b8b5 --- /dev/null +++ b/server/prisma/migrations/20250222043007_init/migration.sql @@ -0,0 +1,12 @@ +-- CreateTable +CREATE TABLE "dept" ( + "deptId" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "parentId" INTEGER, + "ancestors" TEXT, + "deptName" TEXT, + "orderNum" INTEGER, + "status" INTEGER NOT NULL DEFAULT 0, + "delFlag" INTEGER NOT NULL DEFAULT 0, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastUpdatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); diff --git a/server/prisma/migrations/20250224030233_init_test_table/migration.sql b/server/prisma/migrations/20250224030233_init_test_table/migration.sql new file mode 100644 index 0000000..b83b78d --- /dev/null +++ b/server/prisma/migrations/20250224030233_init_test_table/migration.sql @@ -0,0 +1,10 @@ +-- CreateTable +CREATE TABLE "dept_users" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "deptId" INTEGER NOT NULL, + "userId" INTEGER NOT NULL, + "createdAt" DATETIME DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "dept_users_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, + CONSTRAINT "dept_users_deptId_fkey" FOREIGN KEY ("deptId") REFERENCES "dept" ("deptId") ON DELETE CASCADE ON UPDATE NO ACTION +); diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 37c82d4..5244a24 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -2,14 +2,6 @@ generator client { provider = "prisma-client-js" } -// Uncomment the following lines and comment out the SQLite datasource block above to use PostgreSQL -// Make sure to set the correct DATABASE_URL in your .env file -// After swapping run `yarn prisma:setup` from the root directory to migrate the database -// -// datasource db { -// provider = "postgresql" -// url = env("DATABASE_URL") -// } datasource db { provider = "sqlite" url = "file:../storage/anythingllm.db" @@ -30,12 +22,12 @@ model workspace_documents { docpath String workspaceId Int metadata String? - pinned Boolean? @default(false) - watched Boolean? @default(false) createdAt DateTime @default(now()) lastUpdatedAt DateTime @default(now()) - workspace workspaces @relation(fields: [workspaceId], references: [id]) + pinned Boolean? @default(false) + watched Boolean? @default(false) document_sync_queues document_sync_queues? + workspace workspaces @relation(fields: [workspaceId], references: [id]) } model invites { @@ -43,10 +35,10 @@ model invites { code String @unique status String @default("pending") claimedBy Int? - workspaceIds String? createdAt DateTime @default(now()) createdBy Int lastUpdatedAt DateTime @default(now()) + workspaceIds String? } model system_settings { @@ -61,24 +53,25 @@ model users { id Int @id @default(autoincrement()) username String? @unique password String - pfpFilename String? role String @default("default") suspended Int @default(0) - seen_recovery_codes Boolean? @default(false) createdAt DateTime @default(now()) lastUpdatedAt DateTime @default(now()) + pfpFilename String? + seen_recovery_codes Boolean? @default(false) dailyMessageLimit Int? - workspace_chats workspace_chats[] - workspace_users workspace_users[] - embed_configs embed_configs[] + browser_extension_api_keys browser_extension_api_keys[] + dept_users dept_users[] embed_chats embed_chats[] - threads workspace_threads[] - recovery_codes recovery_codes[] + embed_configs embed_configs[] password_reset_tokens password_reset_tokens[] - workspace_agent_invocations workspace_agent_invocations[] + recovery_codes recovery_codes[] slash_command_presets slash_command_presets[] - browser_extension_api_keys browser_extension_api_keys[] temporary_auth_tokens temporary_auth_tokens[] + workspace_agent_invocations workspace_agent_invocations[] + workspace_chats workspace_chats[] + threads workspace_threads[] + workspace_users workspace_users[] } model recovery_codes { @@ -129,21 +122,21 @@ model workspaces { lastUpdatedAt DateTime @default(now()) openAiPrompt String? similarityThreshold Float? @default(0.25) - chatProvider String? chatModel String? topN Int? @default(4) chatMode String? @default("chat") pfpFilename String? - agentProvider String? + chatProvider String? agentModel String? + agentProvider String? queryRefusalResponse String? vectorSearchMode String? @default("default") - workspace_users workspace_users[] + embed_configs embed_configs[] + workspace_agent_invocations workspace_agent_invocations[] documents workspace_documents[] workspace_suggested_messages workspace_suggested_messages[] - embed_configs embed_configs[] threads workspace_threads[] - workspace_agent_invocations workspace_agent_invocations[] + workspace_users workspace_users[] } model workspace_threads { @@ -154,8 +147,8 @@ model workspace_threads { user_id Int? createdAt DateTime @default(now()) lastUpdatedAt DateTime @default(now()) - workspace workspaces @relation(fields: [workspace_id], references: [id], onDelete: Cascade) user users? @relation(fields: [user_id], references: [id], onDelete: Cascade) + workspace workspaces @relation(fields: [workspace_id], references: [id], onDelete: Cascade) @@index([workspace_id]) @@index([user_id]) @@ -180,26 +173,26 @@ model workspace_chats { response String include Boolean @default(true) user_id Int? - thread_id Int? // No relation to prevent whole table migration - api_session_id String? // String identifier for only the dev API to parition chats in any mode. createdAt DateTime @default(now()) lastUpdatedAt DateTime @default(now()) + thread_id Int? feedbackScore Boolean? - users users? @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: Cascade) + api_session_id String? + users users? @relation(fields: [user_id], references: [id], onDelete: Cascade) } model workspace_agent_invocations { id Int @id @default(autoincrement()) uuid String @unique - prompt String // Contains agent invocation to parse + option additional text for seed. + prompt String closed Boolean @default(false) user_id Int? - thread_id Int? // No relation to prevent whole table migration + thread_id Int? workspace_id Int createdAt DateTime @default(now()) lastUpdatedAt DateTime @default(now()) - user users? @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: Cascade) - workspace workspaces @relation(fields: [workspace_id], references: [id], onDelete: Cascade, onUpdate: Cascade) + workspace workspaces @relation(fields: [workspace_id], references: [id], onDelete: Cascade) + user users? @relation(fields: [user_id], references: [id], onDelete: Cascade) @@index([uuid]) } @@ -210,8 +203,8 @@ model workspace_users { workspace_id Int createdAt DateTime @default(now()) lastUpdatedAt DateTime @default(now()) - workspaces workspaces @relation(fields: [workspace_id], references: [id], onDelete: Cascade, onUpdate: Cascade) - users users @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: Cascade) + users users @relation(fields: [user_id], references: [id], onDelete: Cascade) + workspaces workspaces @relation(fields: [workspace_id], references: [id], onDelete: Cascade) } model cache_data { @@ -240,9 +233,9 @@ model embed_configs { createdBy Int? usersId Int? createdAt DateTime @default(now()) - workspace workspaces @relation(fields: [workspace_id], references: [id], onDelete: Cascade) embed_chats embed_chats[] users users? @relation(fields: [usersId], references: [id]) + workspace workspaces @relation(fields: [workspace_id], references: [id], onDelete: Cascade) } model embed_chats { @@ -255,8 +248,8 @@ model embed_chats { embed_id Int usersId Int? createdAt DateTime @default(now()) - embed_config embed_configs @relation(fields: [embed_id], references: [id], onDelete: Cascade) users users? @relation(fields: [usersId], references: [id]) + embed_config embed_configs @relation(fields: [embed_id], references: [id], onDelete: Cascade) } model event_logs { @@ -274,7 +267,7 @@ model slash_command_presets { command String prompt String description String - uid Int @default(0) // 0 is null user + uid Int @default(0) userId Int? createdAt DateTime @default(now()) lastUpdatedAt DateTime @default(now()) @@ -285,13 +278,13 @@ model slash_command_presets { model document_sync_queues { id Int @id @default(autoincrement()) - staleAfterMs Int @default(604800000) // 7 days + staleAfterMs Int @default(604800000) nextSyncAt DateTime createdAt DateTime @default(now()) lastSyncedAt DateTime @default(now()) workspaceDocId Int @unique - workspaceDoc workspace_documents? @relation(fields: [workspaceDocId], references: [id], onDelete: Cascade) runs document_sync_executions[] + workspaceDoc workspace_documents @relation(fields: [workspaceDocId], references: [id], onDelete: Cascade) } model document_sync_executions { @@ -325,3 +318,26 @@ model temporary_auth_tokens { @@index([token]) @@index([userId]) } + +model dept { + deptId Int @id @default(autoincrement()) + parentId Int? + ancestors String? + deptName String? + orderNum Int? + status Int @default(0) + delFlag Int @default(0) + createdAt DateTime @default(now()) + lastUpdatedAt DateTime @default(now()) + dept_users dept_users[] +} + +model dept_users { + id Int @id @default(autoincrement()) + deptId Int + userId Int + createdAt DateTime? @default(now()) + updatedAt DateTime? @default(now()) @updatedAt + user users @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction) + dept dept @relation(fields: [deptId], references: [deptId], onDelete: Cascade, onUpdate: NoAction) +} diff --git a/server/utils/files/index.js b/server/utils/files/index.js index 625d858..8e6e299 100644 --- a/server/utils/files/index.js +++ b/server/utils/files/index.js @@ -32,8 +32,9 @@ async function viewLocalFiles() { type: "folder", items: [], }; - + console.log("111111111111111111111111111111111111111111111111111111111111111111111111111111111"); for (const file of fs.readdirSync(documentsPath)) { + console.log("file:", file); if (path.extname(file) === ".md") continue; const folderPath = path.resolve(documentsPath, file); const isFolder = fs.lstatSync(folderPath).isDirectory(); @@ -45,11 +46,11 @@ async function viewLocalFiles() { }; const subfiles = fs.readdirSync(folderPath); const filenames = {}; - for (const subfile of subfiles) { if (path.extname(subfile) !== ".json") continue; const filePath = path.join(folderPath, subfile); const rawData = fs.readFileSync(filePath, "utf8"); + console.log("rawData:", rawData); const cachefilename = `${file}/${subfile}`; const { pageContent, ...metadata } = JSON.parse(rawData); subdocs.items.push({