You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1166 lines
38 KiB
1166 lines
38 KiB
const path = require("path");
|
|
const fs = require("fs");
|
|
const {
|
|
reqBody,
|
|
multiUserMode,
|
|
userFromSession,
|
|
safeJsonParse,
|
|
} = require("../utils/http");
|
|
const { normalizePath, isWithin } = require("../utils/files");
|
|
const { Workspace } = require("../models/workspace");
|
|
const { Document } = require("../models/documents");
|
|
const { DocumentVectors } = require("../models/vectors");
|
|
const { WorkspaceChats } = require("../models/workspaceChats");
|
|
const { getVectorDbClass } = require("../utils/helpers");
|
|
const { handleFileUpload, handlePfpUpload, handleCommUpload } = require("../utils/files/multer");
|
|
const { validatedRequest } = require("../utils/middleware/validatedRequest");
|
|
const { Telemetry } = require("../models/telemetry");
|
|
const {
|
|
flexUserRoleValid,
|
|
ROLES,
|
|
} = require("../utils/middleware/multiUserProtected");
|
|
const { EventLogs } = require("../models/eventLogs");
|
|
const {
|
|
WorkspaceSuggestedMessages,
|
|
} = require("../models/workspacesSuggestedMessages");
|
|
const { validWorkspaceSlug } = require("../utils/middleware/validWorkspace");
|
|
const { convertToChatHistory } = require("../utils/helpers/chat/responses");
|
|
const { CollectorApi } = require("../utils/collectorApi");
|
|
const {
|
|
determineWorkspacePfpFilepath,
|
|
fetchPfp,
|
|
} = require("../utils/files/pfp");
|
|
const { getTTSProvider } = require("../utils/TextToSpeech");
|
|
const { WorkspaceThread } = require("../models/workspaceThread");
|
|
const truncate = require("truncate");
|
|
const { purgeDocument } = require("../utils/files/purgeDocument");
|
|
const { User } = require("../models/user");
|
|
const { DeptUsers } = require("../models/deptUsers");
|
|
const { DeptDocument } = require("../models/deptDocument");
|
|
const { v4: uuidv4 } = require("uuid");
|
|
const { moveAndRenameFile } = require("../utils/files/index");
|
|
|
|
function workspaceEndpoints(app) {
|
|
if (!app) return;
|
|
|
|
const responseCache = new Map();
|
|
|
|
app.post(
|
|
"/workspace/new",
|
|
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
|
|
async (request, response) => {
|
|
try {
|
|
const user = await userFromSession(request, response);
|
|
const { name = null, onboardingComplete = false } = reqBody(request);
|
|
const { workspace, message } = await Workspace.new(name, user?.id);
|
|
await Telemetry.sendTelemetry(
|
|
"workspace_created",
|
|
{
|
|
multiUserMode: multiUserMode(response),
|
|
LLMSelection: process.env.LLM_PROVIDER || "openai",
|
|
Embedder: process.env.EMBEDDING_ENGINE || "inherit",
|
|
VectorDbSelection: process.env.VECTOR_DB || "lancedb",
|
|
TTSSelection: process.env.TTS_PROVIDER || "native",
|
|
},
|
|
user?.id
|
|
);
|
|
|
|
await EventLogs.logEvent(
|
|
"workspace_created",
|
|
{
|
|
workspaceName: workspace?.name || "Unknown Workspace",
|
|
},
|
|
user?.id
|
|
);
|
|
if (onboardingComplete === true)
|
|
await Telemetry.sendTelemetry("onboarding_complete");
|
|
|
|
response.status(200).json({ workspace, message });
|
|
} catch (e) {
|
|
console.error(e.message, e);
|
|
response.sendStatus(500).end();
|
|
}
|
|
}
|
|
);
|
|
|
|
app.post(
|
|
"/workspace/:slug/update",
|
|
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
|
|
async (request, response) => {
|
|
try {
|
|
const user = await userFromSession(request, response);
|
|
const { slug = null } = request.params;
|
|
const data = reqBody(request);
|
|
const currWorkspace = multiUserMode(response)
|
|
? await Workspace.getWithUser(user, { slug })
|
|
: await Workspace.get({ slug });
|
|
|
|
if (!currWorkspace) {
|
|
response.sendStatus(400).end();
|
|
return;
|
|
}
|
|
await Workspace.trackChange(currWorkspace, data, user);
|
|
const { workspace, message } = await Workspace.update(
|
|
currWorkspace.id,
|
|
data
|
|
);
|
|
response.status(200).json({ workspace, message });
|
|
} catch (e) {
|
|
console.error(e.message, e);
|
|
response.sendStatus(500).end();
|
|
}
|
|
}
|
|
);
|
|
|
|
// app.post(
|
|
// "/workspace/:slug/upload",
|
|
// [
|
|
// validatedRequest,
|
|
// flexUserRoleValid([ROLES.admin, ROLES.manager]),
|
|
// handleFileUpload,
|
|
// ],
|
|
// async function (request, response) {
|
|
// try {
|
|
// const Collector = new CollectorApi();
|
|
// const { originalname } = request.file;
|
|
// const processingOnline = await Collector.online();
|
|
//
|
|
// if (!processingOnline) {
|
|
// response
|
|
// .status(500)
|
|
// .json({
|
|
// success: false,
|
|
// error: `Document processing API is not online. Document ${originalname} will not be processed automatically.`,
|
|
// })
|
|
// .end();
|
|
// return;
|
|
// }
|
|
//
|
|
// const { success, reason } =
|
|
// await Collector.processDocument(originalname);
|
|
// if (!success) {
|
|
// response.status(500).json({ success: false, error: reason }).end();
|
|
// return;
|
|
// }
|
|
//
|
|
// Collector.log(
|
|
// `Document ${originalname} uploaded processed and successfully. It is now available in documents.`
|
|
// );
|
|
// await Telemetry.sendTelemetry("document_uploaded");
|
|
// await EventLogs.logEvent(
|
|
// "document_uploaded",
|
|
// {
|
|
// documentName: originalname,
|
|
// },
|
|
// response.locals?.user?.id
|
|
// );
|
|
// response.status(200).json({ success: true, error: null });
|
|
// } catch (e) {
|
|
// console.error(e.message, e);
|
|
// response.sendStatus(500).end();
|
|
// }
|
|
// }
|
|
// );
|
|
|
|
// app.post(
|
|
// "/workspace/:slug/upload",
|
|
// [
|
|
// validatedRequest,
|
|
// flexUserRoleValid([ROLES.admin, ROLES.manager]),
|
|
// handleFileUpload,
|
|
// ],
|
|
// async function (request, response) {
|
|
// try {
|
|
// const user = await userFromSession(request, response);
|
|
// const deptUserRecord = await DeptUsers.get({ userId: user.id });
|
|
// if (!deptUserRecord.deptUser) {
|
|
// return response.status(500).json({ success: false, error: "没有发现用户组织机构" });
|
|
// }
|
|
// const Collector = new CollectorApi();
|
|
// const { originalname } = request.file;
|
|
// const processingOnline = await Collector.online();
|
|
//
|
|
// if (!processingOnline) {
|
|
// response
|
|
// .status(500)
|
|
// .json({
|
|
// success: false,
|
|
// error: `Document processing API is not online. Document ${originalname} will not be processed automatically.`,
|
|
// })
|
|
// .end();
|
|
// return;
|
|
// }
|
|
//
|
|
// const { success, reason, documents } =
|
|
// await Collector.processDocument(originalname);
|
|
// if (!success) {
|
|
// response.status(500).json({ success: false, error: reason }).end();
|
|
// return;
|
|
// }
|
|
// // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
// // 假设路径字符串
|
|
// const location = documents[0].location;
|
|
// // 将路径中的反斜杠替换为正斜杠(可选,但通常更通用)
|
|
// const unixStylePath = location.replace(/\\/g, '/');
|
|
// // 找到最后一个目录分隔符的位置
|
|
// const lastIndex = unixStylePath.lastIndexOf('/');
|
|
// // 提取文件名
|
|
// const parsedFileName = unixStylePath.substring(lastIndex + 1);
|
|
// const fileExtension = path.extname(request.file.path).toLowerCase();
|
|
// const sourceFile = path.resolve(__dirname, request.file.destination, request.file.originalname);
|
|
// const targetDir =
|
|
// process.env.NODE_ENV === "development"
|
|
// ? path.resolve(__dirname, `../../server/storage/localFile`)
|
|
// : path.resolve(process.env.STORAGE_DIR, `../../server/storage/localFile`);
|
|
// const newFileName = uuidv4() + fileExtension; // 新文件名
|
|
// moveAndRenameFile(sourceFile, targetDir, newFileName);
|
|
// const deptDocData = {
|
|
// deptId: deptUserRecord.deptUser.deptId,
|
|
// parsedFileName: parsedFileName,
|
|
// parsedFilePath: location,
|
|
// realFileName: originalname,
|
|
// realFileAlias: newFileName,
|
|
// realFilePath: targetDir,
|
|
// };
|
|
// await DeptDocument.create(deptDocData);
|
|
// // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
//
|
|
// Collector.log(
|
|
// `Document ${originalname} uploaded processed and successfully. It is now available in documents.`
|
|
// );
|
|
// await Telemetry.sendTelemetry("document_uploaded");
|
|
// await EventLogs.logEvent(
|
|
// "document_uploaded",
|
|
// {
|
|
// documentName: originalname,
|
|
// },
|
|
// response.locals?.user?.id
|
|
// );
|
|
// response.status(200).json({ success: true, error: null });
|
|
// } catch (e) {
|
|
// console.error(e.message, e);
|
|
// response.sendStatus(500).end();
|
|
// }
|
|
// }
|
|
// );
|
|
|
|
|
|
|
|
app.post(
|
|
"/workspace/:slug/upload",
|
|
[
|
|
validatedRequest,
|
|
flexUserRoleValid([ROLES.admin, ROLES.manager]),
|
|
handleFileUpload,
|
|
],
|
|
async function (request, response) {
|
|
try {
|
|
const user = await userFromSession(request, response);
|
|
const deptUserRecord = await DeptUsers.get({ userId: user.id });
|
|
if (!deptUserRecord.deptUser) {
|
|
return response.status(500).json({ success: false, error: "没有发现用户组织机构" });
|
|
}
|
|
const Collector = new CollectorApi();
|
|
const { originalname } = request.file;
|
|
const processingOnline = await Collector.online();
|
|
|
|
if (!processingOnline) {
|
|
return response.status(500).json({
|
|
success: false,
|
|
error: `Document processing API is not online. Document ${originalname} will not be processed automatically.`,
|
|
});
|
|
}
|
|
|
|
// 处理文档
|
|
const { success, reason, documents, fileContent } =
|
|
await Collector.processDocument(originalname);
|
|
if (!success) {
|
|
return response.status(500).json({ success: false, error: reason });
|
|
}
|
|
|
|
// 确定目标目录
|
|
const targetDir =
|
|
process.env.NODE_ENV === "development"
|
|
? path.resolve(__dirname, "../../server/storage/localFile")
|
|
: path.resolve(process.env.STORAGE_DIR, "localFile");
|
|
|
|
// 确保目标目录存在
|
|
if (!fs.existsSync(targetDir)) {
|
|
fs.mkdirSync(targetDir, { recursive: true }); // 递归创建目录
|
|
}
|
|
|
|
// 保存文件
|
|
const filePath = path.join(targetDir, originalname); // 使用原始文件名
|
|
const fileBuffer = Buffer.from(fileContent, "base64");
|
|
fs.writeFileSync(filePath, fileBuffer);
|
|
|
|
console.log(`文件保存成功!路径:${filePath}`);
|
|
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
// 假设路径字符串
|
|
const location = documents[0].location;
|
|
// 将路径中的反斜杠替换为正斜杠(可选,但通常更通用)
|
|
const unixStylePath = location.replace(/\\/g, '/');
|
|
// 找到最后一个目录分隔符的位置
|
|
const lastIndex = unixStylePath.lastIndexOf('/');
|
|
// 提取文件名
|
|
const parsedFileName = unixStylePath.substring(lastIndex + 1);
|
|
const fileExtension = path.extname(request.file.path).toLowerCase();
|
|
const sourceFile = path.resolve(__dirname, request.file.destination, request.file.originalname);
|
|
const newFileName = uuidv4() + fileExtension; // 新文件名
|
|
moveAndRenameFile(sourceFile, targetDir, newFileName);
|
|
const deptDocData = {
|
|
deptId: deptUserRecord.deptUser.deptId,
|
|
parsedFileName: parsedFileName,
|
|
parsedFilePath: location,
|
|
realFileName: originalname,
|
|
realFileAlias: newFileName,
|
|
realFilePath: targetDir,
|
|
// parsedFileId: targetDir,
|
|
};
|
|
await DeptDocument.create(deptDocData);
|
|
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
|
|
// 记录日志和发送遥测
|
|
Collector.log(
|
|
`Document ${originalname} uploaded, processed, and saved successfully. It is now available in documents.`
|
|
);
|
|
await Telemetry.sendTelemetry("document_uploaded");
|
|
await EventLogs.logEvent(
|
|
"document_uploaded",
|
|
{
|
|
documentName: originalname,
|
|
},
|
|
response.locals?.user?.id
|
|
);
|
|
|
|
// 返回成功响应
|
|
response.status(200).json({ success: true, error: null });
|
|
} catch (e) {
|
|
console.error(e.message, e);
|
|
response.status(500).json({ success: false, error: e.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
app.post(
|
|
"/workspace/:slug/upload-link",
|
|
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
|
|
async (request, response) => {
|
|
try {
|
|
const Collector = new CollectorApi();
|
|
const { link = "" } = reqBody(request);
|
|
const processingOnline = await Collector.online();
|
|
|
|
if (!processingOnline) {
|
|
response
|
|
.status(500)
|
|
.json({
|
|
success: false,
|
|
error: `Document processing API is not online. Link ${link} will not be processed automatically.`,
|
|
})
|
|
.end();
|
|
return;
|
|
}
|
|
|
|
const { success, reason } = await Collector.processLink(link);
|
|
if (!success) {
|
|
response.status(500).json({ success: false, error: reason }).end();
|
|
return;
|
|
}
|
|
|
|
Collector.log(
|
|
`Link ${link} uploaded processed and successfully. It is now available in documents.`
|
|
);
|
|
await Telemetry.sendTelemetry("link_uploaded");
|
|
await EventLogs.logEvent(
|
|
"link_uploaded",
|
|
{ link },
|
|
response.locals?.user?.id
|
|
);
|
|
response.status(200).json({ success: true, error: null });
|
|
} catch (e) {
|
|
console.error(e.message, e);
|
|
response.sendStatus(500).end();
|
|
}
|
|
}
|
|
);
|
|
|
|
app.post(
|
|
"/workspace/:slug/update-embeddings",
|
|
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
|
|
async (request, response) => {
|
|
try {
|
|
const user = await userFromSession(request, response);
|
|
const { slug = null } = request.params;
|
|
const { adds = [], deletes = [] } = reqBody(request);
|
|
const currWorkspace = multiUserMode(response)
|
|
? await Workspace.getWithUser(user, { slug })
|
|
: await Workspace.get({ slug });
|
|
|
|
console.log("adds===============", adds);
|
|
|
|
if (!currWorkspace) {
|
|
response.sendStatus(400).end();
|
|
return;
|
|
}
|
|
|
|
await Document.removeDocuments(
|
|
currWorkspace,
|
|
deletes,
|
|
response.locals?.user?.id
|
|
);
|
|
const { failedToEmbed = [], errors = [] } = await Document.addDocuments(
|
|
currWorkspace,
|
|
adds,
|
|
response.locals?.user?.id
|
|
);
|
|
const updatedWorkspace = await Workspace.get({ id: currWorkspace.id });
|
|
response.status(200).json({
|
|
workspace: updatedWorkspace,
|
|
message:
|
|
failedToEmbed.length > 0
|
|
? `${failedToEmbed.length} documents failed to add.\n\n${errors
|
|
.map((msg) => `${msg}`)
|
|
.join("\n\n")}`
|
|
: null,
|
|
});
|
|
} catch (e) {
|
|
console.error(e.message, e);
|
|
response.sendStatus(500).end();
|
|
}
|
|
}
|
|
);
|
|
|
|
app.delete(
|
|
"/workspace/:slug",
|
|
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
|
|
async (request, response) => {
|
|
try {
|
|
const { slug = "" } = request.params;
|
|
const user = await userFromSession(request, response);
|
|
const VectorDb = getVectorDbClass();
|
|
const workspace = multiUserMode(response)
|
|
? await Workspace.getWithUser(user, { slug })
|
|
: await Workspace.get({ slug });
|
|
|
|
if (!workspace) {
|
|
response.sendStatus(400).end();
|
|
return;
|
|
}
|
|
|
|
await WorkspaceChats.delete({ workspaceId: Number(workspace.id) });
|
|
await DocumentVectors.deleteForWorkspace(workspace.id);
|
|
await Document.delete({ workspaceId: Number(workspace.id) });
|
|
await Workspace.delete({ id: Number(workspace.id) });
|
|
|
|
await EventLogs.logEvent(
|
|
"workspace_deleted",
|
|
{
|
|
workspaceName: workspace?.name || "Unknown Workspace",
|
|
},
|
|
response.locals?.user?.id
|
|
);
|
|
|
|
try {
|
|
await VectorDb["delete-namespace"]({ namespace: slug });
|
|
} catch (e) {
|
|
console.error(e.message);
|
|
}
|
|
response.sendStatus(200).end();
|
|
} catch (e) {
|
|
console.error(e.message, e);
|
|
response.sendStatus(500).end();
|
|
}
|
|
}
|
|
);
|
|
|
|
app.delete(
|
|
"/workspace/:slug/reset-vector-db",
|
|
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
|
|
async (request, response) => {
|
|
try {
|
|
const { slug = "" } = request.params;
|
|
const user = await userFromSession(request, response);
|
|
const VectorDb = getVectorDbClass();
|
|
const workspace = multiUserMode(response)
|
|
? await Workspace.getWithUser(user, { slug })
|
|
: await Workspace.get({ slug });
|
|
|
|
if (!workspace) {
|
|
response.sendStatus(400).end();
|
|
return;
|
|
}
|
|
|
|
await DocumentVectors.deleteForWorkspace(workspace.id);
|
|
await Document.delete({ workspaceId: Number(workspace.id) });
|
|
|
|
await EventLogs.logEvent(
|
|
"workspace_vectors_reset",
|
|
{
|
|
workspaceName: workspace?.name || "Unknown Workspace",
|
|
},
|
|
response.locals?.user?.id
|
|
);
|
|
|
|
try {
|
|
await VectorDb["delete-namespace"]({ namespace: slug });
|
|
} catch (e) {
|
|
console.error(e.message);
|
|
}
|
|
response.sendStatus(200).end();
|
|
} catch (e) {
|
|
console.error(e.message, e);
|
|
response.sendStatus(500).end();
|
|
}
|
|
}
|
|
);
|
|
|
|
app.get(
|
|
"/workspaces",
|
|
[validatedRequest, flexUserRoleValid([ROLES.all])],
|
|
async (request, response) => {
|
|
try {
|
|
const user = await userFromSession(request, response);
|
|
const workspaces = multiUserMode(response)
|
|
? await Workspace.whereWithUser(user)
|
|
: await Workspace.where();
|
|
|
|
response.status(200).json({ workspaces });
|
|
} catch (e) {
|
|
console.error(e.message, e);
|
|
response.sendStatus(500).end();
|
|
}
|
|
}
|
|
);
|
|
|
|
app.get(
|
|
"/workspace/:slug",
|
|
[validatedRequest, flexUserRoleValid([ROLES.all])],
|
|
async (request, response) => {
|
|
try {
|
|
const { slug } = request.params;
|
|
const user = await userFromSession(request, response);
|
|
const workspace = multiUserMode(response)
|
|
? await Workspace.getWithUser(user, { slug })
|
|
: await Workspace.get({ slug });
|
|
console.log("workspace++++++++++++++++++++++++++++", workspace);
|
|
response.status(200).json({ workspace });
|
|
} catch (e) {
|
|
console.error(e.message, e);
|
|
response.sendStatus(500).end();
|
|
}
|
|
}
|
|
);
|
|
|
|
app.get(
|
|
"/workspace/:slug/chats",
|
|
[validatedRequest, flexUserRoleValid([ROLES.all])],
|
|
async (request, response) => {
|
|
try {
|
|
const { slug } = request.params;
|
|
const user = await userFromSession(request, response);
|
|
const workspace = multiUserMode(response)
|
|
? await Workspace.getWithUser(user, { slug })
|
|
: await Workspace.get({ slug });
|
|
|
|
if (!workspace) {
|
|
response.sendStatus(400).end();
|
|
return;
|
|
}
|
|
|
|
const history = multiUserMode(response)
|
|
? await WorkspaceChats.forWorkspaceByUser(workspace.id, user.id)
|
|
: await WorkspaceChats.forWorkspace(workspace.id);
|
|
response.status(200).json({ history: convertToChatHistory(history) });
|
|
} catch (e) {
|
|
console.error(e.message, e);
|
|
response.sendStatus(500).end();
|
|
}
|
|
}
|
|
);
|
|
|
|
app.delete(
|
|
"/workspace/:slug/delete-chats",
|
|
[validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
|
|
async (request, response) => {
|
|
try {
|
|
const { chatIds = [] } = reqBody(request);
|
|
const user = await userFromSession(request, response);
|
|
const workspace = response.locals.workspace;
|
|
|
|
if (!workspace || !Array.isArray(chatIds)) {
|
|
response.sendStatus(400).end();
|
|
return;
|
|
}
|
|
|
|
// This works for both workspace and threads.
|
|
// we simplify this by just looking at workspace<>user overlap
|
|
// since they are all on the same table.
|
|
await WorkspaceChats.delete({
|
|
id: { in: chatIds.map((id) => Number(id)) },
|
|
user_id: user?.id ?? null,
|
|
workspaceId: workspace.id,
|
|
});
|
|
|
|
response.sendStatus(200).end();
|
|
} catch (e) {
|
|
console.error(e.message, e);
|
|
response.sendStatus(500).end();
|
|
}
|
|
}
|
|
);
|
|
|
|
app.delete(
|
|
"/workspace/:slug/delete-edited-chats",
|
|
[validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
|
|
async (request, response) => {
|
|
try {
|
|
const { startingId } = reqBody(request);
|
|
const user = await userFromSession(request, response);
|
|
const workspace = response.locals.workspace;
|
|
|
|
await WorkspaceChats.delete({
|
|
workspaceId: workspace.id,
|
|
thread_id: null,
|
|
user_id: user?.id,
|
|
id: { gte: Number(startingId) },
|
|
});
|
|
|
|
response.sendStatus(200).end();
|
|
} catch (e) {
|
|
console.error(e.message, e);
|
|
response.sendStatus(500).end();
|
|
}
|
|
}
|
|
);
|
|
|
|
app.post(
|
|
"/workspace/:slug/update-chat",
|
|
[validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
|
|
async (request, response) => {
|
|
try {
|
|
const { chatId, newText = null } = reqBody(request);
|
|
if (!newText || !String(newText).trim())
|
|
throw new Error("Cannot save empty response");
|
|
|
|
const user = await userFromSession(request, response);
|
|
const workspace = response.locals.workspace;
|
|
const existingChat = await WorkspaceChats.get({
|
|
workspaceId: workspace.id,
|
|
thread_id: null,
|
|
user_id: user?.id,
|
|
id: Number(chatId),
|
|
});
|
|
if (!existingChat) throw new Error("Invalid chat.");
|
|
|
|
const chatResponse = safeJsonParse(existingChat.response, null);
|
|
if (!chatResponse) throw new Error("Failed to parse chat response");
|
|
|
|
await WorkspaceChats._update(existingChat.id, {
|
|
response: JSON.stringify({
|
|
...chatResponse,
|
|
text: String(newText),
|
|
}),
|
|
});
|
|
|
|
response.sendStatus(200).end();
|
|
} catch (e) {
|
|
console.error(e.message, e);
|
|
response.sendStatus(500).end();
|
|
}
|
|
}
|
|
);
|
|
|
|
app.post(
|
|
"/workspace/:slug/chat-feedback/:chatId",
|
|
[validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
|
|
async (request, response) => {
|
|
try {
|
|
const { chatId } = request.params;
|
|
const { feedback = null } = reqBody(request);
|
|
const existingChat = await WorkspaceChats.get({
|
|
id: Number(chatId),
|
|
workspaceId: response.locals.workspace.id,
|
|
});
|
|
|
|
if (!existingChat) {
|
|
response.status(404).end();
|
|
return;
|
|
}
|
|
|
|
const result = await WorkspaceChats.updateFeedbackScore(
|
|
chatId,
|
|
feedback
|
|
);
|
|
response.status(200).json({ success: result });
|
|
} catch (error) {
|
|
console.error("Error updating chat feedback:", error);
|
|
response.status(500).end();
|
|
}
|
|
}
|
|
);
|
|
|
|
app.get(
|
|
"/workspace/:slug/suggested-messages",
|
|
[validatedRequest, flexUserRoleValid([ROLES.all])],
|
|
async function (request, response) {
|
|
try {
|
|
const { slug } = request.params;
|
|
const suggestedMessages =
|
|
await WorkspaceSuggestedMessages.getMessages(slug);
|
|
response.status(200).json({ success: true, suggestedMessages });
|
|
} catch (error) {
|
|
console.error("Error fetching suggested messages:", error);
|
|
response
|
|
.status(500)
|
|
.json({ success: false, message: "Internal server error" });
|
|
}
|
|
}
|
|
);
|
|
|
|
app.post(
|
|
"/workspace/:slug/suggested-messages",
|
|
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
|
|
async (request, response) => {
|
|
try {
|
|
const { messages = [] } = reqBody(request);
|
|
const { slug } = request.params;
|
|
if (!Array.isArray(messages)) {
|
|
return response.status(400).json({
|
|
success: false,
|
|
message: "Invalid message format. Expected an array of messages.",
|
|
});
|
|
}
|
|
|
|
await WorkspaceSuggestedMessages.saveAll(messages, slug);
|
|
return response.status(200).json({
|
|
success: true,
|
|
message: "Suggested messages saved successfully.",
|
|
});
|
|
} catch (error) {
|
|
console.error("Error processing the suggested messages:", error);
|
|
response.status(500).json({
|
|
success: true,
|
|
message: "Error saving the suggested messages.",
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
app.post(
|
|
"/workspace/:slug/update-pin",
|
|
[
|
|
validatedRequest,
|
|
flexUserRoleValid([ROLES.admin, ROLES.manager]),
|
|
validWorkspaceSlug,
|
|
],
|
|
async (request, response) => {
|
|
try {
|
|
const { docPath, pinStatus = false } = reqBody(request);
|
|
const workspace = response.locals.workspace;
|
|
|
|
const document = await Document.get({
|
|
workspaceId: workspace.id,
|
|
docpath: docPath,
|
|
});
|
|
if (!document) return response.sendStatus(404).end();
|
|
|
|
await Document.update(document.id, { pinned: pinStatus });
|
|
return response.status(200).end();
|
|
} catch (error) {
|
|
console.error("Error processing the pin status update:", error);
|
|
return response.status(500).end();
|
|
}
|
|
}
|
|
);
|
|
|
|
app.get(
|
|
"/workspace/:slug/tts/:chatId",
|
|
[validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
|
|
async function (request, response) {
|
|
try {
|
|
const { chatId } = request.params;
|
|
const workspace = response.locals.workspace;
|
|
const cacheKey = `${workspace.slug}:${chatId}`;
|
|
const wsChat = await WorkspaceChats.get({
|
|
id: Number(chatId),
|
|
workspaceId: workspace.id,
|
|
});
|
|
|
|
const cachedResponse = responseCache.get(cacheKey);
|
|
if (cachedResponse) {
|
|
response.writeHead(200, {
|
|
"Content-Type": cachedResponse.mime || "audio/mpeg",
|
|
});
|
|
response.end(cachedResponse.buffer);
|
|
return;
|
|
}
|
|
|
|
const text = safeJsonParse(wsChat.response, null)?.text;
|
|
if (!text) return response.sendStatus(204).end();
|
|
|
|
const TTSProvider = getTTSProvider();
|
|
const buffer = await TTSProvider.ttsBuffer(text);
|
|
if (buffer === null) return response.sendStatus(204).end();
|
|
|
|
responseCache.set(cacheKey, { buffer, mime: "audio/mpeg" });
|
|
response.writeHead(200, {
|
|
"Content-Type": "audio/mpeg",
|
|
});
|
|
response.end(buffer);
|
|
return;
|
|
} catch (error) {
|
|
console.error("Error processing the TTS request:", error);
|
|
response.status(500).json({ message: "TTS could not be completed" });
|
|
}
|
|
}
|
|
);
|
|
|
|
app.get(
|
|
"/workspace/:slug/pfp",
|
|
[validatedRequest, flexUserRoleValid([ROLES.all])],
|
|
async function (request, response) {
|
|
try {
|
|
const { slug } = request.params;
|
|
const cachedResponse = responseCache.get(slug);
|
|
|
|
if (cachedResponse) {
|
|
response.writeHead(200, {
|
|
"Content-Type": cachedResponse.mime || "image/png",
|
|
});
|
|
response.end(cachedResponse.buffer);
|
|
return;
|
|
}
|
|
|
|
const pfpPath = await determineWorkspacePfpFilepath(slug);
|
|
|
|
if (!pfpPath) {
|
|
response.sendStatus(204).end();
|
|
return;
|
|
}
|
|
|
|
const { found, buffer, mime } = fetchPfp(pfpPath);
|
|
if (!found) {
|
|
response.sendStatus(204).end();
|
|
return;
|
|
}
|
|
|
|
responseCache.set(slug, { buffer, mime });
|
|
|
|
response.writeHead(200, {
|
|
"Content-Type": mime || "image/png",
|
|
});
|
|
response.end(buffer);
|
|
return;
|
|
} catch (error) {
|
|
console.error("Error processing the logo request:", error);
|
|
response.status(500).json({ message: "Internal server error" });
|
|
}
|
|
}
|
|
);
|
|
|
|
app.post(
|
|
"/workspace/:slug/upload-pfp",
|
|
[
|
|
validatedRequest,
|
|
flexUserRoleValid([ROLES.admin, ROLES.manager]),
|
|
handlePfpUpload,
|
|
],
|
|
async function (request, response) {
|
|
try {
|
|
const { slug } = request.params;
|
|
const uploadedFileName = request.randomFileName;
|
|
if (!uploadedFileName) {
|
|
return response.status(400).json({ message: "File upload failed." });
|
|
}
|
|
|
|
const workspaceRecord = await Workspace.get({
|
|
slug,
|
|
});
|
|
|
|
const oldPfpFilename = workspaceRecord.pfpFilename;
|
|
if (oldPfpFilename) {
|
|
const storagePath = path.join(__dirname, "../storage/assets/pfp");
|
|
const oldPfpPath = path.join(
|
|
storagePath,
|
|
normalizePath(workspaceRecord.pfpFilename)
|
|
);
|
|
if (!isWithin(path.resolve(storagePath), path.resolve(oldPfpPath)))
|
|
throw new Error("Invalid path name");
|
|
if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
|
|
}
|
|
|
|
const { workspace, message } = await Workspace._update(
|
|
workspaceRecord.id,
|
|
{
|
|
pfpFilename: uploadedFileName,
|
|
}
|
|
);
|
|
|
|
return response.status(workspace ? 200 : 500).json({
|
|
message: workspace
|
|
? "Profile picture uploaded successfully."
|
|
: message,
|
|
});
|
|
} catch (error) {
|
|
console.error("Error processing the profile picture upload:", error);
|
|
response.status(500).json({ message: "Internal server error" });
|
|
}
|
|
}
|
|
);
|
|
|
|
app.delete(
|
|
"/workspace/:slug/remove-pfp",
|
|
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
|
|
async function (request, response) {
|
|
try {
|
|
const { slug } = request.params;
|
|
const workspaceRecord = await Workspace.get({
|
|
slug,
|
|
});
|
|
const oldPfpFilename = workspaceRecord.pfpFilename;
|
|
|
|
if (oldPfpFilename) {
|
|
const storagePath = path.join(__dirname, "../storage/assets/pfp");
|
|
const oldPfpPath = path.join(
|
|
storagePath,
|
|
normalizePath(oldPfpFilename)
|
|
);
|
|
if (!isWithin(path.resolve(storagePath), path.resolve(oldPfpPath)))
|
|
throw new Error("Invalid path name");
|
|
if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
|
|
}
|
|
|
|
const { workspace, message } = await Workspace._update(
|
|
workspaceRecord.id,
|
|
{
|
|
pfpFilename: null,
|
|
}
|
|
);
|
|
|
|
// Clear the cache
|
|
responseCache.delete(slug);
|
|
|
|
return response.status(workspace ? 200 : 500).json({
|
|
message: workspace
|
|
? "Profile picture removed successfully."
|
|
: message,
|
|
});
|
|
} catch (error) {
|
|
console.error("Error processing the profile picture removal:", error);
|
|
response.status(500).json({ message: "Internal server error" });
|
|
}
|
|
}
|
|
);
|
|
|
|
app.post(
|
|
"/workspace/:slug/thread/fork",
|
|
[validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
|
|
async (request, response) => {
|
|
try {
|
|
const user = await userFromSession(request, response);
|
|
const workspace = response.locals.workspace;
|
|
const { chatId, threadSlug } = reqBody(request);
|
|
if (!chatId)
|
|
return response.status(400).json({ message: "chatId is required" });
|
|
|
|
// Get threadId we are branching from if that request body is sent
|
|
// and is a valid thread slug.
|
|
const threadId = !!threadSlug
|
|
? (
|
|
await WorkspaceThread.get({
|
|
slug: String(threadSlug),
|
|
workspace_id: workspace.id,
|
|
})
|
|
)?.id ?? null
|
|
: null;
|
|
const chatsToFork = await WorkspaceChats.where(
|
|
{
|
|
workspaceId: workspace.id,
|
|
user_id: user?.id,
|
|
include: true, // only duplicate visible chats
|
|
thread_id: threadId,
|
|
api_session_id: null, // Do not include API session chats.
|
|
id: { lte: Number(chatId) },
|
|
},
|
|
null,
|
|
{ id: "asc" }
|
|
);
|
|
|
|
const { thread: newThread, message: threadError } =
|
|
await WorkspaceThread.new(workspace, user?.id);
|
|
if (threadError)
|
|
return response.status(500).json({ error: threadError });
|
|
|
|
let lastMessageText = "";
|
|
const chatsData = chatsToFork.map((chat) => {
|
|
const chatResponse = safeJsonParse(chat.response, {});
|
|
if (chatResponse?.text) lastMessageText = chatResponse.text;
|
|
|
|
return {
|
|
workspaceId: workspace.id,
|
|
prompt: chat.prompt,
|
|
response: JSON.stringify(chatResponse),
|
|
user_id: user?.id,
|
|
thread_id: newThread.id,
|
|
};
|
|
});
|
|
await WorkspaceChats.bulkCreate(chatsData);
|
|
await WorkspaceThread.update(newThread, {
|
|
name: !!lastMessageText
|
|
? truncate(lastMessageText, 22)
|
|
: "Forked Thread",
|
|
});
|
|
|
|
await Telemetry.sendTelemetry("thread_forked");
|
|
await EventLogs.logEvent(
|
|
"thread_forked",
|
|
{
|
|
workspaceName: workspace?.name || "Unknown Workspace",
|
|
threadName: newThread.name,
|
|
},
|
|
user?.id
|
|
);
|
|
response.status(200).json({ newThreadSlug: newThread.slug });
|
|
} catch (e) {
|
|
console.error(e.message, e);
|
|
response.status(500).json({ message: "Internal server error" });
|
|
}
|
|
}
|
|
);
|
|
|
|
app.put(
|
|
"/workspace/workspace-chats/:id",
|
|
[validatedRequest, flexUserRoleValid([ROLES.all])],
|
|
async (request, response) => {
|
|
try {
|
|
const { id } = request.params;
|
|
const user = await userFromSession(request, response);
|
|
const validChat = await WorkspaceChats.get({
|
|
id: Number(id),
|
|
user_id: user?.id ?? null,
|
|
});
|
|
if (!validChat)
|
|
return response
|
|
.status(404)
|
|
.json({ success: false, error: "Chat not found." });
|
|
|
|
await WorkspaceChats._update(validChat.id, { include: false });
|
|
response.json({ success: true, error: null });
|
|
} catch (e) {
|
|
console.error(e.message, e);
|
|
response.status(500).json({ success: false, error: "Server error" });
|
|
}
|
|
}
|
|
);
|
|
|
|
/** Handles the uploading and embedding in one-call by uploading via drag-and-drop in chat container. */
|
|
app.post(
|
|
"/workspace/:slug/upload-and-embed",
|
|
[
|
|
validatedRequest,
|
|
flexUserRoleValid([ROLES.admin, ROLES.manager]),
|
|
handleFileUpload,
|
|
],
|
|
async function (request, response) {
|
|
try {
|
|
const { slug = null } = request.params;
|
|
const user = await userFromSession(request, response);
|
|
const currWorkspace = multiUserMode(response)
|
|
? await Workspace.getWithUser(user, { slug })
|
|
: await Workspace.get({ slug });
|
|
|
|
if (!currWorkspace) {
|
|
response.sendStatus(400).end();
|
|
return;
|
|
}
|
|
|
|
const Collector = new CollectorApi();
|
|
const { originalname } = request.file;
|
|
const processingOnline = await Collector.online();
|
|
|
|
if (!processingOnline) {
|
|
response
|
|
.status(500)
|
|
.json({
|
|
success: false,
|
|
error: `Document processing API is not online. Document ${originalname} will not be processed automatically.`,
|
|
})
|
|
.end();
|
|
return;
|
|
}
|
|
|
|
const { success, reason, documents } =
|
|
await Collector.processDocument(originalname);
|
|
if (!success || documents?.length === 0) {
|
|
response.status(500).json({ success: false, error: reason }).end();
|
|
return;
|
|
}
|
|
|
|
Collector.log(
|
|
`Document ${originalname} uploaded processed and successfully. It is now available in documents.`
|
|
);
|
|
await Telemetry.sendTelemetry("document_uploaded");
|
|
await EventLogs.logEvent(
|
|
"document_uploaded",
|
|
{
|
|
documentName: originalname,
|
|
},
|
|
response.locals?.user?.id
|
|
);
|
|
|
|
const document = documents[0];
|
|
const { failedToEmbed = [], errors = [] } = await Document.addDocuments(
|
|
currWorkspace,
|
|
[document.location],
|
|
response.locals?.user?.id
|
|
);
|
|
|
|
if (failedToEmbed.length > 0)
|
|
return response
|
|
.status(200)
|
|
.json({ success: false, error: errors?.[0], document: null });
|
|
|
|
response.status(200).json({
|
|
success: true,
|
|
error: null,
|
|
document: { id: document.id, location: document.location },
|
|
});
|
|
} catch (e) {
|
|
console.error(e.message, e);
|
|
response.sendStatus(500).end();
|
|
}
|
|
}
|
|
);
|
|
|
|
app.delete(
|
|
"/workspace/:slug/remove-and-unembed",
|
|
[
|
|
validatedRequest,
|
|
flexUserRoleValid([ROLES.admin, ROLES.manager]),
|
|
handleFileUpload,
|
|
],
|
|
async function (request, response) {
|
|
try {
|
|
const { slug = null } = request.params;
|
|
const body = reqBody(request);
|
|
const user = await userFromSession(request, response);
|
|
const currWorkspace = multiUserMode(response)
|
|
? await Workspace.getWithUser(user, { slug })
|
|
: await Workspace.get({ slug });
|
|
|
|
if (!currWorkspace || !body.documentLocation)
|
|
return response.sendStatus(400).end();
|
|
|
|
// Will delete the document from the entire system + wil unembed it.
|
|
await purgeDocument(body.documentLocation);
|
|
response.status(200).end();
|
|
} catch (e) {
|
|
console.error(e.message, e);
|
|
response.sendStatus(500).end();
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
module.exports = { workspaceEndpoints };
|