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

1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
  1. const path = require("path");
  2. const fs = require("fs");
  3. const {
  4. reqBody,
  5. multiUserMode,
  6. userFromSession,
  7. safeJsonParse,
  8. } = require("../utils/http");
  9. const { normalizePath, isWithin } = require("../utils/files");
  10. const { Workspace } = require("../models/workspace");
  11. const { Document } = require("../models/documents");
  12. const { DocumentVectors } = require("../models/vectors");
  13. const { WorkspaceChats } = require("../models/workspaceChats");
  14. const { getVectorDbClass } = require("../utils/helpers");
  15. const { handleFileUpload, handlePfpUpload, handleCommUpload } = require("../utils/files/multer");
  16. const { validatedRequest } = require("../utils/middleware/validatedRequest");
  17. const { Telemetry } = require("../models/telemetry");
  18. const {
  19. flexUserRoleValid,
  20. ROLES,
  21. } = require("../utils/middleware/multiUserProtected");
  22. const { EventLogs } = require("../models/eventLogs");
  23. const {
  24. WorkspaceSuggestedMessages,
  25. } = require("../models/workspacesSuggestedMessages");
  26. const { validWorkspaceSlug } = require("../utils/middleware/validWorkspace");
  27. const { convertToChatHistory } = require("../utils/helpers/chat/responses");
  28. const { CollectorApi } = require("../utils/collectorApi");
  29. const {
  30. determineWorkspacePfpFilepath,
  31. fetchPfp,
  32. } = require("../utils/files/pfp");
  33. const { getTTSProvider } = require("../utils/TextToSpeech");
  34. const { WorkspaceThread } = require("../models/workspaceThread");
  35. const truncate = require("truncate");
  36. const { purgeDocument } = require("../utils/files/purgeDocument");
  37. const { User } = require("../models/user");
  38. const { DeptUsers } = require("../models/deptUsers");
  39. const { DeptDocument } = require("../models/deptDocument");
  40. const { v4: uuidv4 } = require("uuid");
  41. const { moveAndRenameFile } = require("../utils/files/index");
  42. function workspaceEndpoints(app) {
  43. if (!app) return;
  44. const responseCache = new Map();
  45. app.post(
  46. "/workspace/new",
  47. [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
  48. async (request, response) => {
  49. try {
  50. const user = await userFromSession(request, response);
  51. const { name = null, onboardingComplete = false } = reqBody(request);
  52. const { workspace, message } = await Workspace.new(name, user?.id);
  53. await Telemetry.sendTelemetry(
  54. "workspace_created",
  55. {
  56. multiUserMode: multiUserMode(response),
  57. LLMSelection: process.env.LLM_PROVIDER || "openai",
  58. Embedder: process.env.EMBEDDING_ENGINE || "inherit",
  59. VectorDbSelection: process.env.VECTOR_DB || "lancedb",
  60. TTSSelection: process.env.TTS_PROVIDER || "native",
  61. },
  62. user?.id
  63. );
  64. await EventLogs.logEvent(
  65. "workspace_created",
  66. {
  67. workspaceName: workspace?.name || "Unknown Workspace",
  68. },
  69. user?.id
  70. );
  71. if (onboardingComplete === true)
  72. await Telemetry.sendTelemetry("onboarding_complete");
  73. response.status(200).json({ workspace, message });
  74. } catch (e) {
  75. console.error(e.message, e);
  76. response.sendStatus(500).end();
  77. }
  78. }
  79. );
  80. app.post(
  81. "/workspace/:slug/update",
  82. [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
  83. async (request, response) => {
  84. try {
  85. const user = await userFromSession(request, response);
  86. const { slug = null } = request.params;
  87. const data = reqBody(request);
  88. const currWorkspace = multiUserMode(response)
  89. ? await Workspace.getWithUser(user, { slug })
  90. : await Workspace.get({ slug });
  91. if (!currWorkspace) {
  92. response.sendStatus(400).end();
  93. return;
  94. }
  95. await Workspace.trackChange(currWorkspace, data, user);
  96. const { workspace, message } = await Workspace.update(
  97. currWorkspace.id,
  98. data
  99. );
  100. response.status(200).json({ workspace, message });
  101. } catch (e) {
  102. console.error(e.message, e);
  103. response.sendStatus(500).end();
  104. }
  105. }
  106. );
  107. // app.post(
  108. // "/workspace/:slug/upload",
  109. // [
  110. // validatedRequest,
  111. // flexUserRoleValid([ROLES.admin, ROLES.manager]),
  112. // handleFileUpload,
  113. // ],
  114. // async function (request, response) {
  115. // try {
  116. // const Collector = new CollectorApi();
  117. // const { originalname } = request.file;
  118. // const processingOnline = await Collector.online();
  119. //
  120. // if (!processingOnline) {
  121. // response
  122. // .status(500)
  123. // .json({
  124. // success: false,
  125. // error: `Document processing API is not online. Document ${originalname} will not be processed automatically.`,
  126. // })
  127. // .end();
  128. // return;
  129. // }
  130. //
  131. // const { success, reason } =
  132. // await Collector.processDocument(originalname);
  133. // if (!success) {
  134. // response.status(500).json({ success: false, error: reason }).end();
  135. // return;
  136. // }
  137. //
  138. // Collector.log(
  139. // `Document ${originalname} uploaded processed and successfully. It is now available in documents.`
  140. // );
  141. // await Telemetry.sendTelemetry("document_uploaded");
  142. // await EventLogs.logEvent(
  143. // "document_uploaded",
  144. // {
  145. // documentName: originalname,
  146. // },
  147. // response.locals?.user?.id
  148. // );
  149. // response.status(200).json({ success: true, error: null });
  150. // } catch (e) {
  151. // console.error(e.message, e);
  152. // response.sendStatus(500).end();
  153. // }
  154. // }
  155. // );
  156. // app.post(
  157. // "/workspace/:slug/upload",
  158. // [
  159. // validatedRequest,
  160. // flexUserRoleValid([ROLES.admin, ROLES.manager]),
  161. // handleFileUpload,
  162. // ],
  163. // async function (request, response) {
  164. // try {
  165. // const user = await userFromSession(request, response);
  166. // const deptUserRecord = await DeptUsers.get({ userId: user.id });
  167. // if (!deptUserRecord.deptUser) {
  168. // return response.status(500).json({ success: false, error: "没有发现用户组织机构" });
  169. // }
  170. // const Collector = new CollectorApi();
  171. // const { originalname } = request.file;
  172. // const processingOnline = await Collector.online();
  173. //
  174. // if (!processingOnline) {
  175. // response
  176. // .status(500)
  177. // .json({
  178. // success: false,
  179. // error: `Document processing API is not online. Document ${originalname} will not be processed automatically.`,
  180. // })
  181. // .end();
  182. // return;
  183. // }
  184. //
  185. // const { success, reason, documents } =
  186. // await Collector.processDocument(originalname);
  187. // if (!success) {
  188. // response.status(500).json({ success: false, error: reason }).end();
  189. // return;
  190. // }
  191. // // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  192. // // 假设路径字符串
  193. // const location = documents[0].location;
  194. // // 将路径中的反斜杠替换为正斜杠(可选,但通常更通用)
  195. // const unixStylePath = location.replace(/\\/g, '/');
  196. // // 找到最后一个目录分隔符的位置
  197. // const lastIndex = unixStylePath.lastIndexOf('/');
  198. // // 提取文件名
  199. // const parsedFileName = unixStylePath.substring(lastIndex + 1);
  200. // const fileExtension = path.extname(request.file.path).toLowerCase();
  201. // const sourceFile = path.resolve(__dirname, request.file.destination, request.file.originalname);
  202. // const targetDir =
  203. // process.env.NODE_ENV === "development"
  204. // ? path.resolve(__dirname, `../../server/storage/localFile`)
  205. // : path.resolve(process.env.STORAGE_DIR, `../../server/storage/localFile`);
  206. // const newFileName = uuidv4() + fileExtension; // 新文件名
  207. // moveAndRenameFile(sourceFile, targetDir, newFileName);
  208. // const deptDocData = {
  209. // deptId: deptUserRecord.deptUser.deptId,
  210. // parsedFileName: parsedFileName,
  211. // parsedFilePath: location,
  212. // realFileName: originalname,
  213. // realFileAlias: newFileName,
  214. // realFilePath: targetDir,
  215. // };
  216. // await DeptDocument.create(deptDocData);
  217. // // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  218. //
  219. // Collector.log(
  220. // `Document ${originalname} uploaded processed and successfully. It is now available in documents.`
  221. // );
  222. // await Telemetry.sendTelemetry("document_uploaded");
  223. // await EventLogs.logEvent(
  224. // "document_uploaded",
  225. // {
  226. // documentName: originalname,
  227. // },
  228. // response.locals?.user?.id
  229. // );
  230. // response.status(200).json({ success: true, error: null });
  231. // } catch (e) {
  232. // console.error(e.message, e);
  233. // response.sendStatus(500).end();
  234. // }
  235. // }
  236. // );
  237. app.post(
  238. "/workspace/:slug/upload",
  239. [
  240. validatedRequest,
  241. flexUserRoleValid([ROLES.admin, ROLES.manager]),
  242. handleFileUpload,
  243. ],
  244. async function (request, response) {
  245. try {
  246. const user = await userFromSession(request, response);
  247. const deptUserRecord = await DeptUsers.get({ userId: user.id });
  248. if (!deptUserRecord.deptUser) {
  249. return response.status(500).json({ success: false, error: "没有发现用户组织机构" });
  250. }
  251. const Collector = new CollectorApi();
  252. const { originalname } = request.file;
  253. const processingOnline = await Collector.online();
  254. if (!processingOnline) {
  255. return response.status(500).json({
  256. success: false,
  257. error: `Document processing API is not online. Document ${originalname} will not be processed automatically.`,
  258. });
  259. }
  260. // 处理文档
  261. const { success, reason, documents, fileContent } =
  262. await Collector.processDocument(originalname);
  263. if (!success) {
  264. return response.status(500).json({ success: false, error: reason });
  265. }
  266. // 确定目标目录
  267. const targetDir =
  268. process.env.NODE_ENV === "development"
  269. ? path.resolve(__dirname, "../../server/storage/localFile")
  270. : path.resolve(process.env.STORAGE_DIR, "localFile");
  271. // 确保目标目录存在
  272. if (!fs.existsSync(targetDir)) {
  273. fs.mkdirSync(targetDir, { recursive: true }); // 递归创建目录
  274. }
  275. // 保存文件
  276. const filePath = path.join(targetDir, originalname); // 使用原始文件名
  277. const fileBuffer = Buffer.from(fileContent, "base64");
  278. fs.writeFileSync(filePath, fileBuffer);
  279. console.log(`文件保存成功!路径:${filePath}`);
  280. // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  281. // 假设路径字符串
  282. const location = documents[0].location;
  283. // 将路径中的反斜杠替换为正斜杠(可选,但通常更通用)
  284. const unixStylePath = location.replace(/\\/g, '/');
  285. // 找到最后一个目录分隔符的位置
  286. const lastIndex = unixStylePath.lastIndexOf('/');
  287. // 提取文件名
  288. const parsedFileName = unixStylePath.substring(lastIndex + 1);
  289. const fileExtension = path.extname(request.file.path).toLowerCase();
  290. const sourceFile = path.resolve(__dirname, request.file.destination, request.file.originalname);
  291. const newFileName = uuidv4() + fileExtension; // 新文件名
  292. moveAndRenameFile(sourceFile, targetDir, newFileName);
  293. const deptDocData = {
  294. deptId: deptUserRecord.deptUser.deptId,
  295. parsedFileName: parsedFileName,
  296. parsedFilePath: location,
  297. realFileName: originalname,
  298. realFileAlias: newFileName,
  299. realFilePath: targetDir,
  300. // parsedFileId: targetDir,
  301. };
  302. await DeptDocument.create(deptDocData);
  303. // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  304. // 记录日志和发送遥测
  305. Collector.log(
  306. `Document ${originalname} uploaded, processed, and saved successfully. It is now available in documents.`
  307. );
  308. await Telemetry.sendTelemetry("document_uploaded");
  309. await EventLogs.logEvent(
  310. "document_uploaded",
  311. {
  312. documentName: originalname,
  313. },
  314. response.locals?.user?.id
  315. );
  316. // 返回成功响应
  317. response.status(200).json({ success: true, error: null });
  318. } catch (e) {
  319. console.error(e.message, e);
  320. response.status(500).json({ success: false, error: e.message });
  321. }
  322. }
  323. );
  324. app.post(
  325. "/workspace/:slug/upload-link",
  326. [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
  327. async (request, response) => {
  328. try {
  329. const Collector = new CollectorApi();
  330. const { link = "" } = reqBody(request);
  331. const processingOnline = await Collector.online();
  332. if (!processingOnline) {
  333. response
  334. .status(500)
  335. .json({
  336. success: false,
  337. error: `Document processing API is not online. Link ${link} will not be processed automatically.`,
  338. })
  339. .end();
  340. return;
  341. }
  342. const { success, reason } = await Collector.processLink(link);
  343. if (!success) {
  344. response.status(500).json({ success: false, error: reason }).end();
  345. return;
  346. }
  347. Collector.log(
  348. `Link ${link} uploaded processed and successfully. It is now available in documents.`
  349. );
  350. await Telemetry.sendTelemetry("link_uploaded");
  351. await EventLogs.logEvent(
  352. "link_uploaded",
  353. { link },
  354. response.locals?.user?.id
  355. );
  356. response.status(200).json({ success: true, error: null });
  357. } catch (e) {
  358. console.error(e.message, e);
  359. response.sendStatus(500).end();
  360. }
  361. }
  362. );
  363. app.post(
  364. "/workspace/:slug/update-embeddings",
  365. [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
  366. async (request, response) => {
  367. try {
  368. const user = await userFromSession(request, response);
  369. const { slug = null } = request.params;
  370. const { adds = [], deletes = [] } = reqBody(request);
  371. const currWorkspace = multiUserMode(response)
  372. ? await Workspace.getWithUser(user, { slug })
  373. : await Workspace.get({ slug });
  374. console.log("adds===============", adds);
  375. if (!currWorkspace) {
  376. response.sendStatus(400).end();
  377. return;
  378. }
  379. await Document.removeDocuments(
  380. currWorkspace,
  381. deletes,
  382. response.locals?.user?.id
  383. );
  384. const { failedToEmbed = [], errors = [] } = await Document.addDocuments(
  385. currWorkspace,
  386. adds,
  387. response.locals?.user?.id
  388. );
  389. const updatedWorkspace = await Workspace.get({ id: currWorkspace.id });
  390. response.status(200).json({
  391. workspace: updatedWorkspace,
  392. message:
  393. failedToEmbed.length > 0
  394. ? `${failedToEmbed.length} documents failed to add.\n\n${errors
  395. .map((msg) => `${msg}`)
  396. .join("\n\n")}`
  397. : null,
  398. });
  399. } catch (e) {
  400. console.error(e.message, e);
  401. response.sendStatus(500).end();
  402. }
  403. }
  404. );
  405. app.delete(
  406. "/workspace/:slug",
  407. [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
  408. async (request, response) => {
  409. try {
  410. const { slug = "" } = request.params;
  411. const user = await userFromSession(request, response);
  412. const VectorDb = getVectorDbClass();
  413. const workspace = multiUserMode(response)
  414. ? await Workspace.getWithUser(user, { slug })
  415. : await Workspace.get({ slug });
  416. if (!workspace) {
  417. response.sendStatus(400).end();
  418. return;
  419. }
  420. await WorkspaceChats.delete({ workspaceId: Number(workspace.id) });
  421. await DocumentVectors.deleteForWorkspace(workspace.id);
  422. await Document.delete({ workspaceId: Number(workspace.id) });
  423. await Workspace.delete({ id: Number(workspace.id) });
  424. await EventLogs.logEvent(
  425. "workspace_deleted",
  426. {
  427. workspaceName: workspace?.name || "Unknown Workspace",
  428. },
  429. response.locals?.user?.id
  430. );
  431. try {
  432. await VectorDb["delete-namespace"]({ namespace: slug });
  433. } catch (e) {
  434. console.error(e.message);
  435. }
  436. response.sendStatus(200).end();
  437. } catch (e) {
  438. console.error(e.message, e);
  439. response.sendStatus(500).end();
  440. }
  441. }
  442. );
  443. app.delete(
  444. "/workspace/:slug/reset-vector-db",
  445. [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
  446. async (request, response) => {
  447. try {
  448. const { slug = "" } = request.params;
  449. const user = await userFromSession(request, response);
  450. const VectorDb = getVectorDbClass();
  451. const workspace = multiUserMode(response)
  452. ? await Workspace.getWithUser(user, { slug })
  453. : await Workspace.get({ slug });
  454. if (!workspace) {
  455. response.sendStatus(400).end();
  456. return;
  457. }
  458. await DocumentVectors.deleteForWorkspace(workspace.id);
  459. await Document.delete({ workspaceId: Number(workspace.id) });
  460. await EventLogs.logEvent(
  461. "workspace_vectors_reset",
  462. {
  463. workspaceName: workspace?.name || "Unknown Workspace",
  464. },
  465. response.locals?.user?.id
  466. );
  467. try {
  468. await VectorDb["delete-namespace"]({ namespace: slug });
  469. } catch (e) {
  470. console.error(e.message);
  471. }
  472. response.sendStatus(200).end();
  473. } catch (e) {
  474. console.error(e.message, e);
  475. response.sendStatus(500).end();
  476. }
  477. }
  478. );
  479. app.get(
  480. "/workspaces",
  481. [validatedRequest, flexUserRoleValid([ROLES.all])],
  482. async (request, response) => {
  483. try {
  484. const user = await userFromSession(request, response);
  485. const workspaces = multiUserMode(response)
  486. ? await Workspace.whereWithUser(user)
  487. : await Workspace.where();
  488. response.status(200).json({ workspaces });
  489. } catch (e) {
  490. console.error(e.message, e);
  491. response.sendStatus(500).end();
  492. }
  493. }
  494. );
  495. app.get(
  496. "/workspace/:slug",
  497. [validatedRequest, flexUserRoleValid([ROLES.all])],
  498. async (request, response) => {
  499. try {
  500. const { slug } = request.params;
  501. const user = await userFromSession(request, response);
  502. const workspace = multiUserMode(response)
  503. ? await Workspace.getWithUser(user, { slug })
  504. : await Workspace.get({ slug });
  505. console.log("workspace++++++++++++++++++++++++++++", workspace);
  506. response.status(200).json({ workspace });
  507. } catch (e) {
  508. console.error(e.message, e);
  509. response.sendStatus(500).end();
  510. }
  511. }
  512. );
  513. app.get(
  514. "/workspace/:slug/chats",
  515. [validatedRequest, flexUserRoleValid([ROLES.all])],
  516. async (request, response) => {
  517. try {
  518. const { slug } = request.params;
  519. const user = await userFromSession(request, response);
  520. const workspace = multiUserMode(response)
  521. ? await Workspace.getWithUser(user, { slug })
  522. : await Workspace.get({ slug });
  523. if (!workspace) {
  524. response.sendStatus(400).end();
  525. return;
  526. }
  527. const history = multiUserMode(response)
  528. ? await WorkspaceChats.forWorkspaceByUser(workspace.id, user.id)
  529. : await WorkspaceChats.forWorkspace(workspace.id);
  530. response.status(200).json({ history: convertToChatHistory(history) });
  531. } catch (e) {
  532. console.error(e.message, e);
  533. response.sendStatus(500).end();
  534. }
  535. }
  536. );
  537. app.delete(
  538. "/workspace/:slug/delete-chats",
  539. [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
  540. async (request, response) => {
  541. try {
  542. const { chatIds = [] } = reqBody(request);
  543. const user = await userFromSession(request, response);
  544. const workspace = response.locals.workspace;
  545. if (!workspace || !Array.isArray(chatIds)) {
  546. response.sendStatus(400).end();
  547. return;
  548. }
  549. // This works for both workspace and threads.
  550. // we simplify this by just looking at workspace<>user overlap
  551. // since they are all on the same table.
  552. await WorkspaceChats.delete({
  553. id: { in: chatIds.map((id) => Number(id)) },
  554. user_id: user?.id ?? null,
  555. workspaceId: workspace.id,
  556. });
  557. response.sendStatus(200).end();
  558. } catch (e) {
  559. console.error(e.message, e);
  560. response.sendStatus(500).end();
  561. }
  562. }
  563. );
  564. app.delete(
  565. "/workspace/:slug/delete-edited-chats",
  566. [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
  567. async (request, response) => {
  568. try {
  569. const { startingId } = reqBody(request);
  570. const user = await userFromSession(request, response);
  571. const workspace = response.locals.workspace;
  572. await WorkspaceChats.delete({
  573. workspaceId: workspace.id,
  574. thread_id: null,
  575. user_id: user?.id,
  576. id: { gte: Number(startingId) },
  577. });
  578. response.sendStatus(200).end();
  579. } catch (e) {
  580. console.error(e.message, e);
  581. response.sendStatus(500).end();
  582. }
  583. }
  584. );
  585. app.post(
  586. "/workspace/:slug/update-chat",
  587. [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
  588. async (request, response) => {
  589. try {
  590. const { chatId, newText = null } = reqBody(request);
  591. if (!newText || !String(newText).trim())
  592. throw new Error("Cannot save empty response");
  593. const user = await userFromSession(request, response);
  594. const workspace = response.locals.workspace;
  595. const existingChat = await WorkspaceChats.get({
  596. workspaceId: workspace.id,
  597. thread_id: null,
  598. user_id: user?.id,
  599. id: Number(chatId),
  600. });
  601. if (!existingChat) throw new Error("Invalid chat.");
  602. const chatResponse = safeJsonParse(existingChat.response, null);
  603. if (!chatResponse) throw new Error("Failed to parse chat response");
  604. await WorkspaceChats._update(existingChat.id, {
  605. response: JSON.stringify({
  606. ...chatResponse,
  607. text: String(newText),
  608. }),
  609. });
  610. response.sendStatus(200).end();
  611. } catch (e) {
  612. console.error(e.message, e);
  613. response.sendStatus(500).end();
  614. }
  615. }
  616. );
  617. app.post(
  618. "/workspace/:slug/chat-feedback/:chatId",
  619. [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
  620. async (request, response) => {
  621. try {
  622. const { chatId } = request.params;
  623. const { feedback = null } = reqBody(request);
  624. const existingChat = await WorkspaceChats.get({
  625. id: Number(chatId),
  626. workspaceId: response.locals.workspace.id,
  627. });
  628. if (!existingChat) {
  629. response.status(404).end();
  630. return;
  631. }
  632. const result = await WorkspaceChats.updateFeedbackScore(
  633. chatId,
  634. feedback
  635. );
  636. response.status(200).json({ success: result });
  637. } catch (error) {
  638. console.error("Error updating chat feedback:", error);
  639. response.status(500).end();
  640. }
  641. }
  642. );
  643. app.get(
  644. "/workspace/:slug/suggested-messages",
  645. [validatedRequest, flexUserRoleValid([ROLES.all])],
  646. async function (request, response) {
  647. try {
  648. const { slug } = request.params;
  649. const suggestedMessages =
  650. await WorkspaceSuggestedMessages.getMessages(slug);
  651. response.status(200).json({ success: true, suggestedMessages });
  652. } catch (error) {
  653. console.error("Error fetching suggested messages:", error);
  654. response
  655. .status(500)
  656. .json({ success: false, message: "Internal server error" });
  657. }
  658. }
  659. );
  660. app.post(
  661. "/workspace/:slug/suggested-messages",
  662. [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
  663. async (request, response) => {
  664. try {
  665. const { messages = [] } = reqBody(request);
  666. const { slug } = request.params;
  667. if (!Array.isArray(messages)) {
  668. return response.status(400).json({
  669. success: false,
  670. message: "Invalid message format. Expected an array of messages.",
  671. });
  672. }
  673. await WorkspaceSuggestedMessages.saveAll(messages, slug);
  674. return response.status(200).json({
  675. success: true,
  676. message: "Suggested messages saved successfully.",
  677. });
  678. } catch (error) {
  679. console.error("Error processing the suggested messages:", error);
  680. response.status(500).json({
  681. success: true,
  682. message: "Error saving the suggested messages.",
  683. });
  684. }
  685. }
  686. );
  687. app.post(
  688. "/workspace/:slug/update-pin",
  689. [
  690. validatedRequest,
  691. flexUserRoleValid([ROLES.admin, ROLES.manager]),
  692. validWorkspaceSlug,
  693. ],
  694. async (request, response) => {
  695. try {
  696. const { docPath, pinStatus = false } = reqBody(request);
  697. const workspace = response.locals.workspace;
  698. const document = await Document.get({
  699. workspaceId: workspace.id,
  700. docpath: docPath,
  701. });
  702. if (!document) return response.sendStatus(404).end();
  703. await Document.update(document.id, { pinned: pinStatus });
  704. return response.status(200).end();
  705. } catch (error) {
  706. console.error("Error processing the pin status update:", error);
  707. return response.status(500).end();
  708. }
  709. }
  710. );
  711. app.get(
  712. "/workspace/:slug/tts/:chatId",
  713. [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
  714. async function (request, response) {
  715. try {
  716. const { chatId } = request.params;
  717. const workspace = response.locals.workspace;
  718. const cacheKey = `${workspace.slug}:${chatId}`;
  719. const wsChat = await WorkspaceChats.get({
  720. id: Number(chatId),
  721. workspaceId: workspace.id,
  722. });
  723. const cachedResponse = responseCache.get(cacheKey);
  724. if (cachedResponse) {
  725. response.writeHead(200, {
  726. "Content-Type": cachedResponse.mime || "audio/mpeg",
  727. });
  728. response.end(cachedResponse.buffer);
  729. return;
  730. }
  731. const text = safeJsonParse(wsChat.response, null)?.text;
  732. if (!text) return response.sendStatus(204).end();
  733. const TTSProvider = getTTSProvider();
  734. const buffer = await TTSProvider.ttsBuffer(text);
  735. if (buffer === null) return response.sendStatus(204).end();
  736. responseCache.set(cacheKey, { buffer, mime: "audio/mpeg" });
  737. response.writeHead(200, {
  738. "Content-Type": "audio/mpeg",
  739. });
  740. response.end(buffer);
  741. return;
  742. } catch (error) {
  743. console.error("Error processing the TTS request:", error);
  744. response.status(500).json({ message: "TTS could not be completed" });
  745. }
  746. }
  747. );
  748. app.get(
  749. "/workspace/:slug/pfp",
  750. [validatedRequest, flexUserRoleValid([ROLES.all])],
  751. async function (request, response) {
  752. try {
  753. const { slug } = request.params;
  754. const cachedResponse = responseCache.get(slug);
  755. if (cachedResponse) {
  756. response.writeHead(200, {
  757. "Content-Type": cachedResponse.mime || "image/png",
  758. });
  759. response.end(cachedResponse.buffer);
  760. return;
  761. }
  762. const pfpPath = await determineWorkspacePfpFilepath(slug);
  763. if (!pfpPath) {
  764. response.sendStatus(204).end();
  765. return;
  766. }
  767. const { found, buffer, mime } = fetchPfp(pfpPath);
  768. if (!found) {
  769. response.sendStatus(204).end();
  770. return;
  771. }
  772. responseCache.set(slug, { buffer, mime });
  773. response.writeHead(200, {
  774. "Content-Type": mime || "image/png",
  775. });
  776. response.end(buffer);
  777. return;
  778. } catch (error) {
  779. console.error("Error processing the logo request:", error);
  780. response.status(500).json({ message: "Internal server error" });
  781. }
  782. }
  783. );
  784. app.post(
  785. "/workspace/:slug/upload-pfp",
  786. [
  787. validatedRequest,
  788. flexUserRoleValid([ROLES.admin, ROLES.manager]),
  789. handlePfpUpload,
  790. ],
  791. async function (request, response) {
  792. try {
  793. const { slug } = request.params;
  794. const uploadedFileName = request.randomFileName;
  795. if (!uploadedFileName) {
  796. return response.status(400).json({ message: "File upload failed." });
  797. }
  798. const workspaceRecord = await Workspace.get({
  799. slug,
  800. });
  801. const oldPfpFilename = workspaceRecord.pfpFilename;
  802. if (oldPfpFilename) {
  803. const storagePath = path.join(__dirname, "../storage/assets/pfp");
  804. const oldPfpPath = path.join(
  805. storagePath,
  806. normalizePath(workspaceRecord.pfpFilename)
  807. );
  808. if (!isWithin(path.resolve(storagePath), path.resolve(oldPfpPath)))
  809. throw new Error("Invalid path name");
  810. if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
  811. }
  812. const { workspace, message } = await Workspace._update(
  813. workspaceRecord.id,
  814. {
  815. pfpFilename: uploadedFileName,
  816. }
  817. );
  818. return response.status(workspace ? 200 : 500).json({
  819. message: workspace
  820. ? "Profile picture uploaded successfully."
  821. : message,
  822. });
  823. } catch (error) {
  824. console.error("Error processing the profile picture upload:", error);
  825. response.status(500).json({ message: "Internal server error" });
  826. }
  827. }
  828. );
  829. app.delete(
  830. "/workspace/:slug/remove-pfp",
  831. [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
  832. async function (request, response) {
  833. try {
  834. const { slug } = request.params;
  835. const workspaceRecord = await Workspace.get({
  836. slug,
  837. });
  838. const oldPfpFilename = workspaceRecord.pfpFilename;
  839. if (oldPfpFilename) {
  840. const storagePath = path.join(__dirname, "../storage/assets/pfp");
  841. const oldPfpPath = path.join(
  842. storagePath,
  843. normalizePath(oldPfpFilename)
  844. );
  845. if (!isWithin(path.resolve(storagePath), path.resolve(oldPfpPath)))
  846. throw new Error("Invalid path name");
  847. if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
  848. }
  849. const { workspace, message } = await Workspace._update(
  850. workspaceRecord.id,
  851. {
  852. pfpFilename: null,
  853. }
  854. );
  855. // Clear the cache
  856. responseCache.delete(slug);
  857. return response.status(workspace ? 200 : 500).json({
  858. message: workspace
  859. ? "Profile picture removed successfully."
  860. : message,
  861. });
  862. } catch (error) {
  863. console.error("Error processing the profile picture removal:", error);
  864. response.status(500).json({ message: "Internal server error" });
  865. }
  866. }
  867. );
  868. app.post(
  869. "/workspace/:slug/thread/fork",
  870. [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
  871. async (request, response) => {
  872. try {
  873. const user = await userFromSession(request, response);
  874. const workspace = response.locals.workspace;
  875. const { chatId, threadSlug } = reqBody(request);
  876. if (!chatId)
  877. return response.status(400).json({ message: "chatId is required" });
  878. // Get threadId we are branching from if that request body is sent
  879. // and is a valid thread slug.
  880. const threadId = !!threadSlug
  881. ? (
  882. await WorkspaceThread.get({
  883. slug: String(threadSlug),
  884. workspace_id: workspace.id,
  885. })
  886. )?.id ?? null
  887. : null;
  888. const chatsToFork = await WorkspaceChats.where(
  889. {
  890. workspaceId: workspace.id,
  891. user_id: user?.id,
  892. include: true, // only duplicate visible chats
  893. thread_id: threadId,
  894. api_session_id: null, // Do not include API session chats.
  895. id: { lte: Number(chatId) },
  896. },
  897. null,
  898. { id: "asc" }
  899. );
  900. const { thread: newThread, message: threadError } =
  901. await WorkspaceThread.new(workspace, user?.id);
  902. if (threadError)
  903. return response.status(500).json({ error: threadError });
  904. let lastMessageText = "";
  905. const chatsData = chatsToFork.map((chat) => {
  906. const chatResponse = safeJsonParse(chat.response, {});
  907. if (chatResponse?.text) lastMessageText = chatResponse.text;
  908. return {
  909. workspaceId: workspace.id,
  910. prompt: chat.prompt,
  911. response: JSON.stringify(chatResponse),
  912. user_id: user?.id,
  913. thread_id: newThread.id,
  914. };
  915. });
  916. await WorkspaceChats.bulkCreate(chatsData);
  917. await WorkspaceThread.update(newThread, {
  918. name: !!lastMessageText
  919. ? truncate(lastMessageText, 22)
  920. : "Forked Thread",
  921. });
  922. await Telemetry.sendTelemetry("thread_forked");
  923. await EventLogs.logEvent(
  924. "thread_forked",
  925. {
  926. workspaceName: workspace?.name || "Unknown Workspace",
  927. threadName: newThread.name,
  928. },
  929. user?.id
  930. );
  931. response.status(200).json({ newThreadSlug: newThread.slug });
  932. } catch (e) {
  933. console.error(e.message, e);
  934. response.status(500).json({ message: "Internal server error" });
  935. }
  936. }
  937. );
  938. app.put(
  939. "/workspace/workspace-chats/:id",
  940. [validatedRequest, flexUserRoleValid([ROLES.all])],
  941. async (request, response) => {
  942. try {
  943. const { id } = request.params;
  944. const user = await userFromSession(request, response);
  945. const validChat = await WorkspaceChats.get({
  946. id: Number(id),
  947. user_id: user?.id ?? null,
  948. });
  949. if (!validChat)
  950. return response
  951. .status(404)
  952. .json({ success: false, error: "Chat not found." });
  953. await WorkspaceChats._update(validChat.id, { include: false });
  954. response.json({ success: true, error: null });
  955. } catch (e) {
  956. console.error(e.message, e);
  957. response.status(500).json({ success: false, error: "Server error" });
  958. }
  959. }
  960. );
  961. /** Handles the uploading and embedding in one-call by uploading via drag-and-drop in chat container. */
  962. app.post(
  963. "/workspace/:slug/upload-and-embed",
  964. [
  965. validatedRequest,
  966. flexUserRoleValid([ROLES.admin, ROLES.manager]),
  967. handleFileUpload,
  968. ],
  969. async function (request, response) {
  970. try {
  971. const { slug = null } = request.params;
  972. const user = await userFromSession(request, response);
  973. const currWorkspace = multiUserMode(response)
  974. ? await Workspace.getWithUser(user, { slug })
  975. : await Workspace.get({ slug });
  976. if (!currWorkspace) {
  977. response.sendStatus(400).end();
  978. return;
  979. }
  980. const Collector = new CollectorApi();
  981. const { originalname } = request.file;
  982. const processingOnline = await Collector.online();
  983. if (!processingOnline) {
  984. response
  985. .status(500)
  986. .json({
  987. success: false,
  988. error: `Document processing API is not online. Document ${originalname} will not be processed automatically.`,
  989. })
  990. .end();
  991. return;
  992. }
  993. const { success, reason, documents } =
  994. await Collector.processDocument(originalname);
  995. if (!success || documents?.length === 0) {
  996. response.status(500).json({ success: false, error: reason }).end();
  997. return;
  998. }
  999. Collector.log(
  1000. `Document ${originalname} uploaded processed and successfully. It is now available in documents.`
  1001. );
  1002. await Telemetry.sendTelemetry("document_uploaded");
  1003. await EventLogs.logEvent(
  1004. "document_uploaded",
  1005. {
  1006. documentName: originalname,
  1007. },
  1008. response.locals?.user?.id
  1009. );
  1010. const document = documents[0];
  1011. const { failedToEmbed = [], errors = [] } = await Document.addDocuments(
  1012. currWorkspace,
  1013. [document.location],
  1014. response.locals?.user?.id
  1015. );
  1016. if (failedToEmbed.length > 0)
  1017. return response
  1018. .status(200)
  1019. .json({ success: false, error: errors?.[0], document: null });
  1020. response.status(200).json({
  1021. success: true,
  1022. error: null,
  1023. document: { id: document.id, location: document.location },
  1024. });
  1025. } catch (e) {
  1026. console.error(e.message, e);
  1027. response.sendStatus(500).end();
  1028. }
  1029. }
  1030. );
  1031. app.delete(
  1032. "/workspace/:slug/remove-and-unembed",
  1033. [
  1034. validatedRequest,
  1035. flexUserRoleValid([ROLES.admin, ROLES.manager]),
  1036. handleFileUpload,
  1037. ],
  1038. async function (request, response) {
  1039. try {
  1040. const { slug = null } = request.params;
  1041. const body = reqBody(request);
  1042. const user = await userFromSession(request, response);
  1043. const currWorkspace = multiUserMode(response)
  1044. ? await Workspace.getWithUser(user, { slug })
  1045. : await Workspace.get({ slug });
  1046. if (!currWorkspace || !body.documentLocation)
  1047. return response.sendStatus(400).end();
  1048. // Will delete the document from the entire system + wil unembed it.
  1049. await purgeDocument(body.documentLocation);
  1050. response.status(200).end();
  1051. } catch (e) {
  1052. console.error(e.message, e);
  1053. response.sendStatus(500).end();
  1054. }
  1055. }
  1056. );
  1057. }
  1058. module.exports = { workspaceEndpoints };