* initialize * expand tool result text limit | add syntax highlighting and json formatting to tool result rendering * fix onError jsdoc * lint * fix unread icon * route protection * improve form handling for NewJobModal * safeJsonParse * remove unneeded comments * remove trycatch * add truncateText helper * add explicit fallback value tos safeJsonParse * add shared cron constant and helpers * reduce frontend indirection * use isLight to compute syntax highlighting theme * remove dead code * remove forJob and make job limit to 50 * create recomputeNextRunAt helper method * add comment about nextRunAt recomputation * add job queue and concurrency control to scheduled jobs * use p-queue * change default max concurrent value to 1 * add comment explaining internal scheduling system * add recomputeNextRunAt on boot * add generated documents to run details * Modify toolsOverride functionality where no tools selected means no tools are given to the agent add a select all/deselect all toggle button for easily selecting all tools in the cerate job form * create usePolling hook * add polling to scheduled jobs and scheduled job runs pages * add cron generation feature in job form * remove cron generation feature | add cron builder feature | add max active scheduled jobs limit * set MAX_ACTIVE to null * replace hour and minute input fields with input with type time * simplify * organize components * move components to bottom of page component * change Generated Documents to Generated Files * add i18n to cronstrue * add i18n * add type="button" to button elements * refactor fileSource retrieval logic * one scheduled job run can have status "running" * add protection of file retrieveal from scheduled job in multiuser mode * fix comments * make job status default to queued * add queued status * fix bug with result trace rendering * store timeout ref and clearTimeout once race settles * remove unneeded handlerPromise tracking * move imports to top level * refactor hardcoded paths to path resolve functions * implement new job form design * simplify * fix button styles * fix runJob bug * implement styles for scheduled jobs page * apply dark mode figma styles * delete unused translation key * implement light mode for new new job modal, run history, and run details * lint * fix light mode scroll bar in tool call card * adjust table header contrast * fix type in subtitle * kill workers when job is in-flight before deleting job * add border-none to buttons * change locale time to iso string * import BackgroundService module level | instatiate backgroundService singltone once and reuse across handlers * add p-queue, @breejs/later and cron-validate as core deps * parse cron expression to a builder state once * add theme to day buttons in cron builder * fix stale tools selection caption * flip popover when popover clips screen height * make ScheduleJob.trigger() await the run insertion | disable run now button if job is in flight * regen table * refactor generated file card * refactor frontend * remove logs * major refactor for tool picking, fix bree/later bug * combine action endpoints, move contine to method * fix unoptimized query with include + take + order * fix dangerous use, refactor job to utils * add copy content to text response * improve notification system subscription for browser * remove unused translations * prevent gen-file cleanup job from deleting active job file generated references * rich text copy * Scheduled Jobs: Translations (#5482) * add locales for scheduled jobs * i18n --------- Co-authored-by: Timothy Carambat <rambat1010@gmail.com> * add config flag with UI notice * update README * telemetry datapoints * Always use UTC on backend, convert to local in frontend * fix tz render * Add job killing * cleanup thinking text in job notifications and break out reasoning in response text. Also hide zero metrics since that is useless * Port generatedFile schema to the normalized workspace chat `outputs` file format so porting to thread is simple and implem between chats <> jobs is 1:1 * what the fuck * compiled bug * fixed thinking oddity in complied frontend * supress multi-toast * fix duration call * Revert "fix duration call" This reverts commit 0491bc71f4223e65ea4046561b15b268fefb8da2. * revert and reapply fix --------- Co-authored-by: Timothy Carambat <rambat1010@gmail.com>
179 lines
5.7 KiB
JavaScript
179 lines
5.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 { ScheduledJobRun } = require("../models/scheduledJobRun");
|
|
const createFilesLib = require("../utils/agents/aibitat/plugins/create-files/lib");
|
|
const { Telemetry } = require("../models/telemetry");
|
|
|
|
/**
|
|
* 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 or scheduled job run that references this file
|
|
const fileSource = await findFileSource(filename, {
|
|
user,
|
|
isMultiUser: multiUserMode(response),
|
|
});
|
|
|
|
if (!fileSource) {
|
|
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(
|
|
fileSource.displayFilename || filename
|
|
);
|
|
response.setHeader("Content-Type", mimeType);
|
|
response.setHeader(
|
|
"Content-Disposition",
|
|
`attachment; filename="${safeFilename}"`
|
|
);
|
|
response.setHeader("Content-Length", fileData.buffer.length);
|
|
response.send(fileData.buffer);
|
|
Telemetry.sendTelemetry("agent_generated_file_downloaded", {
|
|
type: mimeType,
|
|
}).catch(() => {});
|
|
return;
|
|
} catch (error) {
|
|
console.error("[agentFileServer] Download error:", error.message);
|
|
return response.status(500).json({ error: "Failed to download file" });
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Locates the source record (a workspace chat or a scheduled job run) that
|
|
* references the given storage filename, and confirms the requester has access.
|
|
*
|
|
* Search order:
|
|
* 1. Workspace chats the user can access (per multi-user permissions).
|
|
* 2. Scheduled job runs — single-user only, so no per-user access check.
|
|
*
|
|
* @param {string} storageFilename
|
|
* @param {{ user: object|null, isMultiUser: boolean }} ctx
|
|
* @returns {Promise<{workspaceId: number|null, displayFilename: string}|null>}
|
|
*/
|
|
async function findFileSource(storageFilename, { user, isMultiUser }) {
|
|
try {
|
|
const fromChat = await findInWorkspaceChats(storageFilename, {
|
|
user,
|
|
isMultiUser,
|
|
});
|
|
if (fromChat) return fromChat;
|
|
|
|
if (isMultiUser) return null;
|
|
|
|
return await findInScheduledJobRuns(storageFilename);
|
|
} catch (error) {
|
|
console.error("[findFileSource] Error:", error.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Search workspace chats the user has access to. In single-user mode all
|
|
// workspaces are accessible; in multi-user mode only workspaces assigned to
|
|
// the user are. Returns the matching chat's workspace + display filename.
|
|
async function findInWorkspaceChats(storageFilename, { user, isMultiUser }) {
|
|
const workspaces =
|
|
isMultiUser && user
|
|
? await Workspace.whereWithUser(user)
|
|
: await Workspace.where();
|
|
|
|
const workspaceIds = workspaces.map((w) => w.id);
|
|
if (workspaceIds.length === 0) return null;
|
|
|
|
// DB-level filter so we don't load every chat into memory.
|
|
const chats = await WorkspaceChats.where({
|
|
workspaceId: { in: workspaceIds },
|
|
include: true,
|
|
response: { contains: storageFilename },
|
|
});
|
|
|
|
for (const chat of chats) {
|
|
const { outputs = [] } = safeJsonParse(chat.response, { outputs: [] });
|
|
const output = outputs.find(
|
|
(o) => o?.payload?.storageFilename === storageFilename
|
|
);
|
|
if (!output) continue;
|
|
return {
|
|
workspaceId: chat.workspaceId,
|
|
displayFilename:
|
|
output.payload.filename || output.payload.displayFilename,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Search completed scheduled job runs. Scheduled jobs are single-user only,
|
|
// so this skips access control. Returns the matching run's display filename.
|
|
async function findInScheduledJobRuns(storageFilename) {
|
|
const runs = await ScheduledJobRun.where({
|
|
status: "completed",
|
|
result: { contains: storageFilename },
|
|
});
|
|
|
|
for (const run of runs) {
|
|
const { outputs = [] } = safeJsonParse(run.result, { outputs: [] });
|
|
const output = outputs.find(
|
|
(o) => o?.payload?.storageFilename === storageFilename
|
|
);
|
|
if (!output) continue;
|
|
return {
|
|
workspaceId: null,
|
|
displayFilename: output.payload.filename || storageFilename,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
module.exports = { agentFileServerEndpoints };
|