u
This commit is contained in:
@@ -18,6 +18,7 @@ import { getOrCreateCert } from '../util/cert.js';
|
||||
import { createLogger } from '../util/logger.js';
|
||||
import v1Router from '../v1/index.js';
|
||||
import { startBackendMonitor, stopBackendMonitor, isDevMode } from '../backendmonitor/server.js';
|
||||
import { startBackupService, stopBackupService } from '../services/backupService.js';
|
||||
|
||||
const apiLog = createLogger('api');
|
||||
|
||||
@@ -245,6 +246,12 @@ export async function startApi() {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
startBackupService();
|
||||
} catch (err) {
|
||||
apiLog.error('Failed to start Backup Service:', err.message);
|
||||
}
|
||||
|
||||
return server;
|
||||
})();
|
||||
|
||||
@@ -267,6 +274,7 @@ export async function stopApi() {
|
||||
await shutdownImageCropWorkers();
|
||||
// Same for the pooled sandbox threads that run `javascript_execute` / applets.
|
||||
await shutdownJavascriptSandboxWorkers();
|
||||
await stopBackupService();
|
||||
const closers = [];
|
||||
if (server) {
|
||||
const s = server;
|
||||
|
||||
72
src/api/routes/backup.js
Normal file
72
src/api/routes/backup.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import { listBackups, getBackupServiceStatus, createHourlyBackup } from '../../services/backupService.js';
|
||||
|
||||
/**
|
||||
* Registers internal backup service status and execution endpoints.
|
||||
* @param {import('express').Application} app - Express app
|
||||
* @param {string} [basePath=''] - Optional URL prefix for all routes in this module
|
||||
*/
|
||||
export function register(app, basePath = '') {
|
||||
/**
|
||||
* @openapi
|
||||
* /backup:
|
||||
* get:
|
||||
* tags: [Backup]
|
||||
* summary: List backups and service status
|
||||
* description: Retrieves the current status of the internal backup service and lists available hourly, daily, and monthly backups.
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Backup service status and listing
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: object
|
||||
* backups:
|
||||
* type: object
|
||||
* 500:
|
||||
* description: Internal error
|
||||
*/
|
||||
// #region GET /backup
|
||||
// MARK: handles GET /backup to list backups and service status
|
||||
app.get(`${basePath}/backup`, async (_req, res) => {
|
||||
try {
|
||||
const status = getBackupServiceStatus();
|
||||
const backups = listBackups();
|
||||
res.status(200).json({ status, backups });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
// #endregion
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /backup/run:
|
||||
* post:
|
||||
* tags: [Backup]
|
||||
* summary: Trigger manual backup
|
||||
* description: Immediately triggers an on-demand backup snapshot of VFS, storage, and SQLite database.
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Backup triggered successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* 500:
|
||||
* description: Backup execution failure
|
||||
*/
|
||||
// #region POST /backup/run
|
||||
// MARK: handles POST /backup/run to trigger manual backup
|
||||
app.post(`${basePath}/backup/run`, async (_req, res) => {
|
||||
try {
|
||||
const result = await createHourlyBackup({ manual: true });
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
// #endregion
|
||||
}
|
||||
@@ -9,6 +9,9 @@ import env from '../../config/env.js';
|
||||
import * as mcpDb from '../../db/mcp.js';
|
||||
import * as googleAuthDb from '../../db/googleAuth.js';
|
||||
import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from '../../tools/prompts.js';
|
||||
import { createSearchArticlesTool } from '../../tools/search-articles.js';
|
||||
import { createSubagentTool } from '../../tools/subagent.js';
|
||||
import { resolveDefaultModel } from '../../tools/llm-key.js';
|
||||
|
||||
const MAX_RESPONSE_CHARS = 20_000;
|
||||
const REQUEST_TIMEOUT_MS = 30_000;
|
||||
@@ -177,6 +180,89 @@ function createDashbackMcpServer({ user }) {
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
'dash-subagent',
|
||||
{
|
||||
title: 'Dashback Subagent',
|
||||
description: TOOL_DESCRIPTIONS.subagent || 'Delegate a task to an isolated subagent with access to Dashback tools and VFS.',
|
||||
inputSchema: {
|
||||
task: z.string().describe(TOOL_PARAMETER_DESCRIPTIONS.subagent?.task || 'Specific instructions and goal for the subagent.'),
|
||||
skills: z.array(z.string()).nullable().optional().describe(TOOL_PARAMETER_DESCRIPTIONS.subagent?.skills || 'Array of SKILL names or slugs to inject into the subagent prompt.'),
|
||||
conversation_id: z.number().int().positive().nullable().optional().describe(TOOL_PARAMETER_DESCRIPTIONS.subagent?.conversation_id || 'Existing subagent conversation ID to follow up on, or null to create a new one.'),
|
||||
max_iterations: z.number().int().min(1).max(30).nullable().optional().describe(TOOL_PARAMETER_DESCRIPTIONS.subagent?.max_iterations || 'Safety bound on tool loops (default 15, max 30).'),
|
||||
model: z.string().nullable().optional().describe('Optional OpenRouter model override. Defaults to company default model.'),
|
||||
note: z.string().nullable().optional().describe(TOOL_PARAMETER_DESCRIPTIONS.subagent?.note || 'Status message shown during execution.'),
|
||||
},
|
||||
},
|
||||
async ({ task, skills = null, conversation_id = null, max_iterations = null, model = null, note = null }, extra = {}) => {
|
||||
const freshUser = await googleAuthDb.getById(user.id);
|
||||
const companyId = freshUser?.selectedCompanyId || user.selectedCompanyId;
|
||||
if (!companyId) throw new Error('No company selected');
|
||||
|
||||
const parentModel = model || (await resolveDefaultModel(user.id));
|
||||
const tool = createSubagentTool({
|
||||
googleAuthId: user.id,
|
||||
companyId,
|
||||
conversationId: null,
|
||||
parentModel,
|
||||
depth: 0,
|
||||
});
|
||||
|
||||
console.log(`[mcp] Tool invoke dash-subagent: task="${task?.slice(0, 100)}"`);
|
||||
const signals = [AbortSignal.timeout(300_000)];
|
||||
if (extra?.signal) signals.push(extra.signal);
|
||||
const signal = AbortSignal.any(signals);
|
||||
|
||||
const result = await tool.invoke(
|
||||
{ task, skills, conversation_id, max_iterations, note },
|
||||
{ signal }
|
||||
);
|
||||
|
||||
let text = JSON.stringify(result, null, 2);
|
||||
if (text.length > MAX_RESPONSE_CHARS) {
|
||||
text = `${text.slice(0, MAX_RESPONSE_CHARS)}\n…[truncated, ${text.length} chars total]`;
|
||||
}
|
||||
return {
|
||||
content: [{ type: 'text', text }],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
'search-articles',
|
||||
{
|
||||
title: 'Search Articles',
|
||||
description: TOOL_DESCRIPTIONS.search_articles || 'Search articles by name, brand, EAN, SKU, or supplier SKU (AND logic, ordered by name).',
|
||||
inputSchema: {
|
||||
searchquery: z.string().nullable().optional().describe(TOOL_PARAMETER_DESCRIPTIONS.search_articles?.searchquery || 'Keywords matching name, brand, EAN, SKU, or supplier SKU (AND logic).'),
|
||||
excludewords: z.string().nullable().optional().describe(TOOL_PARAMETER_DESCRIPTIONS.search_articles?.excludewords || 'Keywords to exclude (AND logic).'),
|
||||
offset: z.number().int().min(0).nullable().optional().describe(TOOL_PARAMETER_DESCRIPTIONS.search_articles?.offset || 'Pagination offset (default 0).'),
|
||||
limit: z.number().int().min(1).max(20).nullable().optional().describe(TOOL_PARAMETER_DESCRIPTIONS.search_articles?.limit || 'Window size 1–20 (default 3).'),
|
||||
},
|
||||
},
|
||||
async ({ searchquery = '', excludewords = '', offset = 0, limit = 3 } = {}) => {
|
||||
const freshUser = await googleAuthDb.getById(user.id);
|
||||
const companyId = freshUser?.selectedCompanyId || user.selectedCompanyId;
|
||||
if (!companyId) throw new Error('No company selected');
|
||||
|
||||
console.log(`[mcp] Tool invoke search-articles: query="${searchquery ?? ''}" exclude="${excludewords ?? ''}" offset=${offset} limit=${limit}`);
|
||||
const tool = createSearchArticlesTool({ googleAuthId: user.id, companyId });
|
||||
const result = await tool.invoke({
|
||||
searchquery: searchquery ?? '',
|
||||
excludewords: excludewords ?? '',
|
||||
offset: offset ?? 0,
|
||||
limit: limit ?? 3,
|
||||
});
|
||||
let text = JSON.stringify(result, null, 2);
|
||||
if (text.length > MAX_RESPONSE_CHARS) {
|
||||
text = `${text.slice(0, MAX_RESPONSE_CHARS)}\n…[truncated, ${text.length} chars total]`;
|
||||
}
|
||||
return {
|
||||
content: [{ type: 'text', text }],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user