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.

1161 lines
38 KiB

11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months 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. // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  280. // 假设路径字符串
  281. const location = documents[0].location;
  282. // 将路径中的反斜杠替换为正斜杠(可选,但通常更通用)
  283. const unixStylePath = location.replace(/\\/g, '/');
  284. // 找到最后一个目录分隔符的位置
  285. const lastIndex = unixStylePath.lastIndexOf('/');
  286. // 提取文件名
  287. const parsedFileName = unixStylePath.substring(lastIndex + 1);
  288. const fileExtension = path.extname(request.file.path).toLowerCase();
  289. const newFileName = uuidv4() + fileExtension; // 新文件名
  290. const deptDocData = {
  291. deptId: deptUserRecord.deptUser.deptId,
  292. parsedFileName: parsedFileName,
  293. parsedFilePath: location,
  294. realFileName: originalname,
  295. realFileAlias: newFileName,
  296. realFilePath: targetDir,
  297. // parsedFileId: targetDir,
  298. };
  299. await DeptDocument.create(deptDocData);
  300. // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  301. // 记录日志和发送遥测
  302. Collector.log(
  303. `Document ${originalname} uploaded, processed, and saved successfully. It is now available in documents.`
  304. );
  305. await Telemetry.sendTelemetry("document_uploaded");
  306. await EventLogs.logEvent(
  307. "document_uploaded",
  308. {
  309. documentName: originalname,
  310. },
  311. response.locals?.user?.id
  312. );
  313. // 返回成功响应
  314. response.status(200).json({ success: true, error: null });
  315. } catch (e) {
  316. console.error(e.message, e);
  317. response.status(500).json({ success: false, error: e.message });
  318. }
  319. }
  320. );
  321. app.post(
  322. "/workspace/:slug/upload-link",
  323. [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
  324. async (request, response) => {
  325. try {
  326. const Collector = new CollectorApi();
  327. const { link = "" } = reqBody(request);
  328. const processingOnline = await Collector.online();
  329. if (!processingOnline) {
  330. response
  331. .status(500)
  332. .json({
  333. success: false,
  334. error: `Document processing API is not online. Link ${link} will not be processed automatically.`,
  335. })
  336. .end();
  337. return;
  338. }
  339. const { success, reason } = await Collector.processLink(link);
  340. if (!success) {
  341. response.status(500).json({ success: false, error: reason }).end();
  342. return;
  343. }
  344. Collector.log(
  345. `Link ${link} uploaded processed and successfully. It is now available in documents.`
  346. );
  347. await Telemetry.sendTelemetry("link_uploaded");
  348. await EventLogs.logEvent(
  349. "link_uploaded",
  350. { link },
  351. response.locals?.user?.id
  352. );
  353. response.status(200).json({ success: true, error: null });
  354. } catch (e) {
  355. console.error(e.message, e);
  356. response.sendStatus(500).end();
  357. }
  358. }
  359. );
  360. app.post(
  361. "/workspace/:slug/update-embeddings",
  362. [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
  363. async (request, response) => {
  364. try {
  365. const user = await userFromSession(request, response);
  366. const { slug = null } = request.params;
  367. const { adds = [], deletes = [] } = reqBody(request);
  368. const currWorkspace = multiUserMode(response)
  369. ? await Workspace.getWithUser(user, { slug })
  370. : await Workspace.get({ slug });
  371. console.log("adds===============", adds);
  372. if (!currWorkspace) {
  373. response.sendStatus(400).end();
  374. return;
  375. }
  376. await Document.removeDocuments(
  377. currWorkspace,
  378. deletes,
  379. response.locals?.user?.id
  380. );
  381. const { failedToEmbed = [], errors = [] } = await Document.addDocuments(
  382. currWorkspace,
  383. adds,
  384. response.locals?.user?.id
  385. );
  386. const updatedWorkspace = await Workspace.get({ id: currWorkspace.id });
  387. response.status(200).json({
  388. workspace: updatedWorkspace,
  389. message:
  390. failedToEmbed.length > 0
  391. ? `${failedToEmbed.length} documents failed to add.\n\n${errors
  392. .map((msg) => `${msg}`)
  393. .join("\n\n")}`
  394. : null,
  395. });
  396. } catch (e) {
  397. console.error(e.message, e);
  398. response.sendStatus(500).end();
  399. }
  400. }
  401. );
  402. app.delete(
  403. "/workspace/:slug",
  404. [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
  405. async (request, response) => {
  406. try {
  407. const { slug = "" } = request.params;
  408. const user = await userFromSession(request, response);
  409. const VectorDb = getVectorDbClass();
  410. const workspace = multiUserMode(response)
  411. ? await Workspace.getWithUser(user, { slug })
  412. : await Workspace.get({ slug });
  413. if (!workspace) {
  414. response.sendStatus(400).end();
  415. return;
  416. }
  417. await WorkspaceChats.delete({ workspaceId: Number(workspace.id) });
  418. await DocumentVectors.deleteForWorkspace(workspace.id);
  419. await Document.delete({ workspaceId: Number(workspace.id) });
  420. await Workspace.delete({ id: Number(workspace.id) });
  421. await EventLogs.logEvent(
  422. "workspace_deleted",
  423. {
  424. workspaceName: workspace?.name || "Unknown Workspace",
  425. },
  426. response.locals?.user?.id
  427. );
  428. try {
  429. await VectorDb["delete-namespace"]({ namespace: slug });
  430. } catch (e) {
  431. console.error(e.message);
  432. }
  433. response.sendStatus(200).end();
  434. } catch (e) {
  435. console.error(e.message, e);
  436. response.sendStatus(500).end();
  437. }
  438. }
  439. );
  440. app.delete(
  441. "/workspace/:slug/reset-vector-db",
  442. [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
  443. async (request, response) => {
  444. try {
  445. const { slug = "" } = request.params;
  446. const user = await userFromSession(request, response);
  447. const VectorDb = getVectorDbClass();
  448. const workspace = multiUserMode(response)
  449. ? await Workspace.getWithUser(user, { slug })
  450. : await Workspace.get({ slug });
  451. if (!workspace) {
  452. response.sendStatus(400).end();
  453. return;
  454. }
  455. await DocumentVectors.deleteForWorkspace(workspace.id);
  456. await Document.delete({ workspaceId: Number(workspace.id) });
  457. await EventLogs.logEvent(
  458. "workspace_vectors_reset",
  459. {
  460. workspaceName: workspace?.name || "Unknown Workspace",
  461. },
  462. response.locals?.user?.id
  463. );
  464. try {
  465. await VectorDb["delete-namespace"]({ namespace: slug });
  466. } catch (e) {
  467. console.error(e.message);
  468. }
  469. response.sendStatus(200).end();
  470. } catch (e) {
  471. console.error(e.message, e);
  472. response.sendStatus(500).end();
  473. }
  474. }
  475. );
  476. app.get(
  477. "/workspaces",
  478. [validatedRequest, flexUserRoleValid([ROLES.all])],
  479. async (request, response) => {
  480. try {
  481. const user = await userFromSession(request, response);
  482. const workspaces = multiUserMode(response)
  483. ? await Workspace.whereWithUser(user)
  484. : await Workspace.where();
  485. response.status(200).json({ workspaces });
  486. } catch (e) {
  487. console.error(e.message, e);
  488. response.sendStatus(500).end();
  489. }
  490. }
  491. );
  492. app.get(
  493. "/workspace/:slug",
  494. [validatedRequest, flexUserRoleValid([ROLES.all])],
  495. async (request, response) => {
  496. try {
  497. const { slug } = request.params;
  498. const user = await userFromSession(request, response);
  499. const workspace = multiUserMode(response)
  500. ? await Workspace.getWithUser(user, { slug })
  501. : await Workspace.get({ slug });
  502. response.status(200).json({ workspace });
  503. } catch (e) {
  504. console.error(e.message, e);
  505. response.sendStatus(500).end();
  506. }
  507. }
  508. );
  509. app.get(
  510. "/workspace/:slug/chats",
  511. [validatedRequest, flexUserRoleValid([ROLES.all])],
  512. async (request, response) => {
  513. try {
  514. const { slug } = request.params;
  515. const user = await userFromSession(request, response);
  516. const workspace = multiUserMode(response)
  517. ? await Workspace.getWithUser(user, { slug })
  518. : await Workspace.get({ slug });
  519. if (!workspace) {
  520. response.sendStatus(400).end();
  521. return;
  522. }
  523. const history = multiUserMode(response)
  524. ? await WorkspaceChats.forWorkspaceByUser(workspace.id, user.id)
  525. : await WorkspaceChats.forWorkspace(workspace.id);
  526. response.status(200).json({ history: convertToChatHistory(history) });
  527. } catch (e) {
  528. console.error(e.message, e);
  529. response.sendStatus(500).end();
  530. }
  531. }
  532. );
  533. app.delete(
  534. "/workspace/:slug/delete-chats",
  535. [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
  536. async (request, response) => {
  537. try {
  538. const { chatIds = [] } = reqBody(request);
  539. const user = await userFromSession(request, response);
  540. const workspace = response.locals.workspace;
  541. if (!workspace || !Array.isArray(chatIds)) {
  542. response.sendStatus(400).end();
  543. return;
  544. }
  545. // This works for both workspace and threads.
  546. // we simplify this by just looking at workspace<>user overlap
  547. // since they are all on the same table.
  548. await WorkspaceChats.delete({
  549. id: { in: chatIds.map((id) => Number(id)) },
  550. user_id: user?.id ?? null,
  551. workspaceId: workspace.id,
  552. });
  553. response.sendStatus(200).end();
  554. } catch (e) {
  555. console.error(e.message, e);
  556. response.sendStatus(500).end();
  557. }
  558. }
  559. );
  560. app.delete(
  561. "/workspace/:slug/delete-edited-chats",
  562. [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
  563. async (request, response) => {
  564. try {
  565. const { startingId } = reqBody(request);
  566. const user = await userFromSession(request, response);
  567. const workspace = response.locals.workspace;
  568. await WorkspaceChats.delete({
  569. workspaceId: workspace.id,
  570. thread_id: null,
  571. user_id: user?.id,
  572. id: { gte: Number(startingId) },
  573. });
  574. response.sendStatus(200).end();
  575. } catch (e) {
  576. console.error(e.message, e);
  577. response.sendStatus(500).end();
  578. }
  579. }
  580. );
  581. app.post(
  582. "/workspace/:slug/update-chat",
  583. [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
  584. async (request, response) => {
  585. try {
  586. const { chatId, newText = null } = reqBody(request);
  587. if (!newText || !String(newText).trim())
  588. throw new Error("Cannot save empty response");
  589. const user = await userFromSession(request, response);
  590. const workspace = response.locals.workspace;
  591. const existingChat = await WorkspaceChats.get({
  592. workspaceId: workspace.id,
  593. thread_id: null,
  594. user_id: user?.id,
  595. id: Number(chatId),
  596. });
  597. if (!existingChat) throw new Error("Invalid chat.");
  598. const chatResponse = safeJsonParse(existingChat.response, null);
  599. if (!chatResponse) throw new Error("Failed to parse chat response");
  600. await WorkspaceChats._update(existingChat.id, {
  601. response: JSON.stringify({
  602. ...chatResponse,
  603. text: String(newText),
  604. }),
  605. });
  606. response.sendStatus(200).end();
  607. } catch (e) {
  608. console.error(e.message, e);
  609. response.sendStatus(500).end();
  610. }
  611. }
  612. );
  613. app.post(
  614. "/workspace/:slug/chat-feedback/:chatId",
  615. [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
  616. async (request, response) => {
  617. try {
  618. const { chatId } = request.params;
  619. const { feedback = null } = reqBody(request);
  620. const existingChat = await WorkspaceChats.get({
  621. id: Number(chatId),
  622. workspaceId: response.locals.workspace.id,
  623. });
  624. if (!existingChat) {
  625. response.status(404).end();
  626. return;
  627. }
  628. const result = await WorkspaceChats.updateFeedbackScore(
  629. chatId,
  630. feedback
  631. );
  632. response.status(200).json({ success: result });
  633. } catch (error) {
  634. console.error("Error updating chat feedback:", error);
  635. response.status(500).end();
  636. }
  637. }
  638. );
  639. app.get(
  640. "/workspace/:slug/suggested-messages",
  641. [validatedRequest, flexUserRoleValid([ROLES.all])],
  642. async function (request, response) {
  643. try {
  644. const { slug } = request.params;
  645. const suggestedMessages =
  646. await WorkspaceSuggestedMessages.getMessages(slug);
  647. response.status(200).json({ success: true, suggestedMessages });
  648. } catch (error) {
  649. console.error("Error fetching suggested messages:", error);
  650. response
  651. .status(500)
  652. .json({ success: false, message: "Internal server error" });
  653. }
  654. }
  655. );
  656. app.post(
  657. "/workspace/:slug/suggested-messages",
  658. [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
  659. async (request, response) => {
  660. try {
  661. const { messages = [] } = reqBody(request);
  662. const { slug } = request.params;
  663. if (!Array.isArray(messages)) {
  664. return response.status(400).json({
  665. success: false,
  666. message: "Invalid message format. Expected an array of messages.",
  667. });
  668. }
  669. await WorkspaceSuggestedMessages.saveAll(messages, slug);
  670. return response.status(200).json({
  671. success: true,
  672. message: "Suggested messages saved successfully.",
  673. });
  674. } catch (error) {
  675. console.error("Error processing the suggested messages:", error);
  676. response.status(500).json({
  677. success: true,
  678. message: "Error saving the suggested messages.",
  679. });
  680. }
  681. }
  682. );
  683. app.post(
  684. "/workspace/:slug/update-pin",
  685. [
  686. validatedRequest,
  687. flexUserRoleValid([ROLES.admin, ROLES.manager]),
  688. validWorkspaceSlug,
  689. ],
  690. async (request, response) => {
  691. try {
  692. const { docPath, pinStatus = false } = reqBody(request);
  693. const workspace = response.locals.workspace;
  694. const document = await Document.get({
  695. workspaceId: workspace.id,
  696. docpath: docPath,
  697. });
  698. if (!document) return response.sendStatus(404).end();
  699. await Document.update(document.id, { pinned: pinStatus });
  700. return response.status(200).end();
  701. } catch (error) {
  702. console.error("Error processing the pin status update:", error);
  703. return response.status(500).end();
  704. }
  705. }
  706. );
  707. app.get(
  708. "/workspace/:slug/tts/:chatId",
  709. [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
  710. async function (request, response) {
  711. try {
  712. const { chatId } = request.params;
  713. const workspace = response.locals.workspace;
  714. const cacheKey = `${workspace.slug}:${chatId}`;
  715. const wsChat = await WorkspaceChats.get({
  716. id: Number(chatId),
  717. workspaceId: workspace.id,
  718. });
  719. const cachedResponse = responseCache.get(cacheKey);
  720. if (cachedResponse) {
  721. response.writeHead(200, {
  722. "Content-Type": cachedResponse.mime || "audio/mpeg",
  723. });
  724. response.end(cachedResponse.buffer);
  725. return;
  726. }
  727. const text = safeJsonParse(wsChat.response, null)?.text;
  728. if (!text) return response.sendStatus(204).end();
  729. const TTSProvider = getTTSProvider();
  730. const buffer = await TTSProvider.ttsBuffer(text);
  731. if (buffer === null) return response.sendStatus(204).end();
  732. responseCache.set(cacheKey, { buffer, mime: "audio/mpeg" });
  733. response.writeHead(200, {
  734. "Content-Type": "audio/mpeg",
  735. });
  736. response.end(buffer);
  737. return;
  738. } catch (error) {
  739. console.error("Error processing the TTS request:", error);
  740. response.status(500).json({ message: "TTS could not be completed" });
  741. }
  742. }
  743. );
  744. app.get(
  745. "/workspace/:slug/pfp",
  746. [validatedRequest, flexUserRoleValid([ROLES.all])],
  747. async function (request, response) {
  748. try {
  749. const { slug } = request.params;
  750. const cachedResponse = responseCache.get(slug);
  751. if (cachedResponse) {
  752. response.writeHead(200, {
  753. "Content-Type": cachedResponse.mime || "image/png",
  754. });
  755. response.end(cachedResponse.buffer);
  756. return;
  757. }
  758. const pfpPath = await determineWorkspacePfpFilepath(slug);
  759. if (!pfpPath) {
  760. response.sendStatus(204).end();
  761. return;
  762. }
  763. const { found, buffer, mime } = fetchPfp(pfpPath);
  764. if (!found) {
  765. response.sendStatus(204).end();
  766. return;
  767. }
  768. responseCache.set(slug, { buffer, mime });
  769. response.writeHead(200, {
  770. "Content-Type": mime || "image/png",
  771. });
  772. response.end(buffer);
  773. return;
  774. } catch (error) {
  775. console.error("Error processing the logo request:", error);
  776. response.status(500).json({ message: "Internal server error" });
  777. }
  778. }
  779. );
  780. app.post(
  781. "/workspace/:slug/upload-pfp",
  782. [
  783. validatedRequest,
  784. flexUserRoleValid([ROLES.admin, ROLES.manager]),
  785. handlePfpUpload,
  786. ],
  787. async function (request, response) {
  788. try {
  789. const { slug } = request.params;
  790. const uploadedFileName = request.randomFileName;
  791. if (!uploadedFileName) {
  792. return response.status(400).json({ message: "File upload failed." });
  793. }
  794. const workspaceRecord = await Workspace.get({
  795. slug,
  796. });
  797. const oldPfpFilename = workspaceRecord.pfpFilename;
  798. if (oldPfpFilename) {
  799. const storagePath = path.join(__dirname, "../storage/assets/pfp");
  800. const oldPfpPath = path.join(
  801. storagePath,
  802. normalizePath(workspaceRecord.pfpFilename)
  803. );
  804. if (!isWithin(path.resolve(storagePath), path.resolve(oldPfpPath)))
  805. throw new Error("Invalid path name");
  806. if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
  807. }
  808. const { workspace, message } = await Workspace._update(
  809. workspaceRecord.id,
  810. {
  811. pfpFilename: uploadedFileName,
  812. }
  813. );
  814. return response.status(workspace ? 200 : 500).json({
  815. message: workspace
  816. ? "Profile picture uploaded successfully."
  817. : message,
  818. });
  819. } catch (error) {
  820. console.error("Error processing the profile picture upload:", error);
  821. response.status(500).json({ message: "Internal server error" });
  822. }
  823. }
  824. );
  825. app.delete(
  826. "/workspace/:slug/remove-pfp",
  827. [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
  828. async function (request, response) {
  829. try {
  830. const { slug } = request.params;
  831. const workspaceRecord = await Workspace.get({
  832. slug,
  833. });
  834. const oldPfpFilename = workspaceRecord.pfpFilename;
  835. if (oldPfpFilename) {
  836. const storagePath = path.join(__dirname, "../storage/assets/pfp");
  837. const oldPfpPath = path.join(
  838. storagePath,
  839. normalizePath(oldPfpFilename)
  840. );
  841. if (!isWithin(path.resolve(storagePath), path.resolve(oldPfpPath)))
  842. throw new Error("Invalid path name");
  843. if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
  844. }
  845. const { workspace, message } = await Workspace._update(
  846. workspaceRecord.id,
  847. {
  848. pfpFilename: null,
  849. }
  850. );
  851. // Clear the cache
  852. responseCache.delete(slug);
  853. return response.status(workspace ? 200 : 500).json({
  854. message: workspace
  855. ? "Profile picture removed successfully."
  856. : message,
  857. });
  858. } catch (error) {
  859. console.error("Error processing the profile picture removal:", error);
  860. response.status(500).json({ message: "Internal server error" });
  861. }
  862. }
  863. );
  864. app.post(
  865. "/workspace/:slug/thread/fork",
  866. [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
  867. async (request, response) => {
  868. try {
  869. const user = await userFromSession(request, response);
  870. const workspace = response.locals.workspace;
  871. const { chatId, threadSlug } = reqBody(request);
  872. if (!chatId)
  873. return response.status(400).json({ message: "chatId is required" });
  874. // Get threadId we are branching from if that request body is sent
  875. // and is a valid thread slug.
  876. const threadId = !!threadSlug
  877. ? (
  878. await WorkspaceThread.get({
  879. slug: String(threadSlug),
  880. workspace_id: workspace.id,
  881. })
  882. )?.id ?? null
  883. : null;
  884. const chatsToFork = await WorkspaceChats.where(
  885. {
  886. workspaceId: workspace.id,
  887. user_id: user?.id,
  888. include: true, // only duplicate visible chats
  889. thread_id: threadId,
  890. api_session_id: null, // Do not include API session chats.
  891. id: { lte: Number(chatId) },
  892. },
  893. null,
  894. { id: "asc" }
  895. );
  896. const { thread: newThread, message: threadError } =
  897. await WorkspaceThread.new(workspace, user?.id);
  898. if (threadError)
  899. return response.status(500).json({ error: threadError });
  900. let lastMessageText = "";
  901. const chatsData = chatsToFork.map((chat) => {
  902. const chatResponse = safeJsonParse(chat.response, {});
  903. if (chatResponse?.text) lastMessageText = chatResponse.text;
  904. return {
  905. workspaceId: workspace.id,
  906. prompt: chat.prompt,
  907. response: JSON.stringify(chatResponse),
  908. user_id: user?.id,
  909. thread_id: newThread.id,
  910. };
  911. });
  912. await WorkspaceChats.bulkCreate(chatsData);
  913. await WorkspaceThread.update(newThread, {
  914. name: !!lastMessageText
  915. ? truncate(lastMessageText, 22)
  916. : "Forked Thread",
  917. });
  918. await Telemetry.sendTelemetry("thread_forked");
  919. await EventLogs.logEvent(
  920. "thread_forked",
  921. {
  922. workspaceName: workspace?.name || "Unknown Workspace",
  923. threadName: newThread.name,
  924. },
  925. user?.id
  926. );
  927. response.status(200).json({ newThreadSlug: newThread.slug });
  928. } catch (e) {
  929. console.error(e.message, e);
  930. response.status(500).json({ message: "Internal server error" });
  931. }
  932. }
  933. );
  934. app.put(
  935. "/workspace/workspace-chats/:id",
  936. [validatedRequest, flexUserRoleValid([ROLES.all])],
  937. async (request, response) => {
  938. try {
  939. const { id } = request.params;
  940. const user = await userFromSession(request, response);
  941. const validChat = await WorkspaceChats.get({
  942. id: Number(id),
  943. user_id: user?.id ?? null,
  944. });
  945. if (!validChat)
  946. return response
  947. .status(404)
  948. .json({ success: false, error: "Chat not found." });
  949. await WorkspaceChats._update(validChat.id, { include: false });
  950. response.json({ success: true, error: null });
  951. } catch (e) {
  952. console.error(e.message, e);
  953. response.status(500).json({ success: false, error: "Server error" });
  954. }
  955. }
  956. );
  957. /** Handles the uploading and embedding in one-call by uploading via drag-and-drop in chat container. */
  958. app.post(
  959. "/workspace/:slug/upload-and-embed",
  960. [
  961. validatedRequest,
  962. flexUserRoleValid([ROLES.admin, ROLES.manager]),
  963. handleFileUpload,
  964. ],
  965. async function (request, response) {
  966. try {
  967. const { slug = null } = request.params;
  968. const user = await userFromSession(request, response);
  969. const currWorkspace = multiUserMode(response)
  970. ? await Workspace.getWithUser(user, { slug })
  971. : await Workspace.get({ slug });
  972. if (!currWorkspace) {
  973. response.sendStatus(400).end();
  974. return;
  975. }
  976. const Collector = new CollectorApi();
  977. const { originalname } = request.file;
  978. const processingOnline = await Collector.online();
  979. if (!processingOnline) {
  980. response
  981. .status(500)
  982. .json({
  983. success: false,
  984. error: `Document processing API is not online. Document ${originalname} will not be processed automatically.`,
  985. })
  986. .end();
  987. return;
  988. }
  989. const { success, reason, documents } =
  990. await Collector.processDocument(originalname);
  991. if (!success || documents?.length === 0) {
  992. response.status(500).json({ success: false, error: reason }).end();
  993. return;
  994. }
  995. Collector.log(
  996. `Document ${originalname} uploaded processed and successfully. It is now available in documents.`
  997. );
  998. await Telemetry.sendTelemetry("document_uploaded");
  999. await EventLogs.logEvent(
  1000. "document_uploaded",
  1001. {
  1002. documentName: originalname,
  1003. },
  1004. response.locals?.user?.id
  1005. );
  1006. const document = documents[0];
  1007. const { failedToEmbed = [], errors = [] } = await Document.addDocuments(
  1008. currWorkspace,
  1009. [document.location],
  1010. response.locals?.user?.id
  1011. );
  1012. if (failedToEmbed.length > 0)
  1013. return response
  1014. .status(200)
  1015. .json({ success: false, error: errors?.[0], document: null });
  1016. response.status(200).json({
  1017. success: true,
  1018. error: null,
  1019. document: { id: document.id, location: document.location },
  1020. });
  1021. } catch (e) {
  1022. console.error(e.message, e);
  1023. response.sendStatus(500).end();
  1024. }
  1025. }
  1026. );
  1027. app.delete(
  1028. "/workspace/:slug/remove-and-unembed",
  1029. [
  1030. validatedRequest,
  1031. flexUserRoleValid([ROLES.admin, ROLES.manager]),
  1032. handleFileUpload,
  1033. ],
  1034. async function (request, response) {
  1035. try {
  1036. const { slug = null } = request.params;
  1037. const body = reqBody(request);
  1038. const user = await userFromSession(request, response);
  1039. const currWorkspace = multiUserMode(response)
  1040. ? await Workspace.getWithUser(user, { slug })
  1041. : await Workspace.get({ slug });
  1042. if (!currWorkspace || !body.documentLocation)
  1043. return response.sendStatus(400).end();
  1044. // Will delete the document from the entire system + wil unembed it.
  1045. await purgeDocument(body.documentLocation);
  1046. response.status(200).end();
  1047. } catch (e) {
  1048. console.error(e.message, e);
  1049. response.sendStatus(500).end();
  1050. }
  1051. }
  1052. );
  1053. }
  1054. module.exports = { workspaceEndpoints };