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.

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