Files
anything-llm/server/endpoints/agentFileServer.js
Timothy Carambat 7aaea7f514 File creation agent skills (#5280)
* Powerpoint File Creation (#5278)

* wip

* download card

* UI for downloading

* move to fs system with endpoint to pull files

* refactor UI

* final-pass

* remove save-file-browser skill and refactor

* remove fileDownload event

* reset

* reset file

* reset timeout

* persist toggle

* Txt creation (#5279)

* wip

* download card

* UI for downloading

* move to fs system with endpoint to pull files

* refactor UI

* final-pass

* remove save-file-browser skill and refactor

* remove fileDownload event

* reset

* reset file

* reset timeout

* wip

* persist toggle

* add arbitrary text creation file

* Add PDF document generation with markdown formatting (#5283)

add support for branding in bottom right corner
refactor core utils and frontend rendering

* Xlsx document creation (#5284)

add Excel doc & sheet creation

* Basic docx creation (#5285)

* Basic docx creation

* add test theme support + styling and title pages

* simplify skill selection

* handle TG attachments

* send documents over tg

* lazy import

* pin deps

* fix lock

* i18n for file creation (#5286)

i18n for file-creation
connect #5280

* theme overhaul

* Add PPTX subagent for better results

* forgot files

* Add PPTX subagent for better results (#5287)

* Add PPTX subagent for better results

* forgot files

* make sub-agent use proper tool calling if it can and better UI hints
2026-03-30 15:13:39 -07:00

143 lines
4.7 KiB
JavaScript

const {
userFromSession,
multiUserMode,
safeJsonParse,
} = require("../utils/http");
const { validatedRequest } = require("../utils/middleware/validatedRequest");
const {
flexUserRoleValid,
ROLES,
} = require("../utils/middleware/multiUserProtected");
const { WorkspaceChats } = require("../models/workspaceChats");
const { Workspace } = require("../models/workspace");
const createFilesLib = require("../utils/agents/aibitat/plugins/create-files/lib");
/**
* Endpoints for serving agent-generated files (PPTX, etc.) with authentication
* and ownership validation.
*/
function agentFileServerEndpoints(app) {
if (!app) return;
/**
* Download a generated file by its storage filename.
* Validates that the requesting user has access to the workspace
* where the file was generated.
*/
app.get(
"/agent-skills/generated-files/:filename",
[validatedRequest, flexUserRoleValid([ROLES.all])],
async (request, response) => {
try {
const user = await userFromSession(request, response);
const { filename } = request.params;
if (!filename)
return response.status(400).json({ error: "Filename is required" });
// Validate filename format
const parsed = createFilesLib.parseFilename(filename);
if (!parsed) {
return response
.status(400)
.json({ error: "Invalid filename format" });
}
// Find a chat record that references this file and that the user can access
const validChat = await findValidChatForFile(
filename,
user,
multiUserMode(response)
);
if (!validChat) {
return response.status(404).json({
error: "File not found or access denied",
});
}
// Retrieve the file from storage
const fileData = await createFilesLib.getGeneratedFile(filename);
if (!fileData) {
return response
.status(404)
.json({ error: "File not found in storage" });
}
// Get mime type and set headers for download
const mimeType = createFilesLib.getMimeType(`.${parsed.extension}`);
const safeFilename = createFilesLib.sanitizeFilenameForHeader(
validChat.displayFilename || filename
);
response.setHeader("Content-Type", mimeType);
response.setHeader(
"Content-Disposition",
`attachment; filename="${safeFilename}"`
);
response.setHeader("Content-Length", fileData.buffer.length);
return response.send(fileData.buffer);
} catch (error) {
console.error("[agentFileServer] Download error:", error.message);
return response.status(500).json({ error: "Failed to download file" });
}
}
);
}
/**
* Finds a valid chat record that references the given storage filename
* and that the user has access to.
* @param {string} storageFilename - The storage filename to search for
* @param {object|null} user - The user object (null in single-user mode)
* @param {boolean} isMultiUser - Whether multi-user mode is enabled
* @returns {Promise<{workspaceId: number, displayFilename: string}|null>}
*/
async function findValidChatForFile(storageFilename, user, isMultiUser) {
try {
// Get all workspaces the user has access to.
// In single-user mode, all workspaces are accessible.
// In multi-user mode, only workspaces assigned to the user are accessible.
let workspaceIds;
if (isMultiUser && user) {
const workspaces = await Workspace.whereWithUser(user);
workspaceIds = workspaces.map((w) => w.id);
} else {
const workspaces = await Workspace.where();
workspaceIds = workspaces.map((w) => w.id);
}
if (workspaceIds.length === 0) return null;
// Use database-level filtering to only fetch chats that contain the filename
// This avoids loading all chats into memory
const chats = await WorkspaceChats.where({
workspaceId: { in: workspaceIds },
include: true,
response: { contains: storageFilename },
});
for (const chat of chats) {
try {
const response = safeJsonParse(chat.response, { outputs: [] });
const output = response.outputs.find(
(o) => o?.payload?.storageFilename === storageFilename
);
if (!output) continue;
return {
workspaceId: chat.workspaceId,
displayFilename:
output.payload.filename || output.payload.displayFilename,
};
} catch {
continue;
}
}
return null;
} catch (error) {
console.error("[findValidChatForFile] Error:", error.message);
return null;
}
}
module.exports = { agentFileServerEndpoints };