neoagent 2.2.0 → 2.2.1-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/server/db/database.js +35 -0
- package/server/http/routes.js +1 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +71727 -70915
- package/server/routes/widgets.js +101 -0
- package/server/services/ai/engine.js +7 -2
- package/server/services/ai/toolResult.js +25 -0
- package/server/services/ai/tools.js +182 -0
- package/server/services/manager.js +31 -0
- package/server/services/scheduler/cron.js +85 -32
- package/server/services/scheduler/cron_utils.js +216 -0
- package/server/services/voice/bufferedLiveRelayAdapter.js +85 -17
- package/server/services/voice/liveSession.js +109 -9
- package/server/services/voice/providers.js +44 -18
- package/server/services/voice/runtimeManager.js +75 -25
- package/server/services/voice/turnRunner.js +53 -25
- package/server/services/websocket.js +26 -1
- package/server/services/widgets/service.js +550 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const { requireAuth } = require('../middleware/auth');
|
|
4
|
+
const { sanitizeError } = require('../utils/security');
|
|
5
|
+
const { getAgentIdFromRequest, resolveAgentId } = require('../services/agents/manager');
|
|
6
|
+
|
|
7
|
+
router.use(requireAuth);
|
|
8
|
+
|
|
9
|
+
function widgetService(req) {
|
|
10
|
+
return req.app?.locals?.widgetService;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
router.get('/snapshots', (req, res) => {
|
|
14
|
+
try {
|
|
15
|
+
const service = widgetService(req);
|
|
16
|
+
if (!service) {
|
|
17
|
+
return res.status(500).json({ error: 'Widget service unavailable.' });
|
|
18
|
+
}
|
|
19
|
+
const agentId = req.query?.all === 'true'
|
|
20
|
+
? null
|
|
21
|
+
: resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
|
|
22
|
+
res.json(service.listLatestSnapshots(req.session.userId, { agentId }));
|
|
23
|
+
} catch (err) {
|
|
24
|
+
res.status(400).json({ error: sanitizeError(err) });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
router.get('/', (req, res) => {
|
|
29
|
+
try {
|
|
30
|
+
const service = widgetService(req);
|
|
31
|
+
if (!service) {
|
|
32
|
+
return res.status(500).json({ error: 'Widget service unavailable.' });
|
|
33
|
+
}
|
|
34
|
+
const agentId = req.query?.all === 'true'
|
|
35
|
+
? null
|
|
36
|
+
: resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
|
|
37
|
+
res.json(service.listWidgets(req.session.userId, { agentId }));
|
|
38
|
+
} catch (err) {
|
|
39
|
+
res.status(400).json({ error: sanitizeError(err) });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
router.post('/', (req, res) => {
|
|
44
|
+
try {
|
|
45
|
+
const service = widgetService(req);
|
|
46
|
+
if (!service) {
|
|
47
|
+
return res.status(500).json({ error: 'Widget service unavailable.' });
|
|
48
|
+
}
|
|
49
|
+
const widget = service.createWidget(req.session.userId, req.body || {});
|
|
50
|
+
res.status(201).json(widget);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
res.status(400).json({ error: sanitizeError(err) });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
router.put('/:id', (req, res) => {
|
|
57
|
+
try {
|
|
58
|
+
const service = widgetService(req);
|
|
59
|
+
if (!service) {
|
|
60
|
+
return res.status(500).json({ error: 'Widget service unavailable.' });
|
|
61
|
+
}
|
|
62
|
+
const widget = service.updateWidget(req.session.userId, req.params.id, req.body || {});
|
|
63
|
+
res.json(widget);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
res.status(400).json({ error: sanitizeError(err) });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
router.delete('/:id', (req, res) => {
|
|
70
|
+
try {
|
|
71
|
+
const service = widgetService(req);
|
|
72
|
+
if (!service) {
|
|
73
|
+
return res.status(500).json({ error: 'Widget service unavailable.' });
|
|
74
|
+
}
|
|
75
|
+
res.json(service.deleteWidget(req.session.userId, req.params.id));
|
|
76
|
+
} catch (err) {
|
|
77
|
+
res.status(400).json({ error: sanitizeError(err) });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
router.post('/:id/refresh', (req, res) => {
|
|
82
|
+
try {
|
|
83
|
+
const service = widgetService(req);
|
|
84
|
+
const scheduler = req.app?.locals?.scheduler;
|
|
85
|
+
if (!service || !scheduler) {
|
|
86
|
+
return res.status(500).json({ error: 'Widget refresh unavailable.' });
|
|
87
|
+
}
|
|
88
|
+
const widget = service.getWidget(req.session.userId, req.params.id);
|
|
89
|
+
if (!widget) {
|
|
90
|
+
return res.status(404).json({ error: 'Widget not found.' });
|
|
91
|
+
}
|
|
92
|
+
if (!widget.scheduledTaskId) {
|
|
93
|
+
return res.status(400).json({ error: 'Widget is missing its refresh task.' });
|
|
94
|
+
}
|
|
95
|
+
res.json(scheduler.runTaskNow(widget.scheduledTaskId, req.session.userId));
|
|
96
|
+
} catch (err) {
|
|
97
|
+
res.status(400).json({ error: sanitizeError(err) });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
module.exports = router;
|
|
@@ -474,6 +474,10 @@ function classifyToolExecution(toolName, toolArgs = {}, result, errorMessage = '
|
|
|
474
474
|
'update_scheduled_task',
|
|
475
475
|
'delete_scheduled_task',
|
|
476
476
|
'schedule_run',
|
|
477
|
+
'create_ai_widget',
|
|
478
|
+
'update_ai_widget',
|
|
479
|
+
'delete_ai_widget',
|
|
480
|
+
'save_widget_snapshot',
|
|
477
481
|
'mcp_add_server',
|
|
478
482
|
'mcp_remove_server',
|
|
479
483
|
'spawn_subagent',
|
|
@@ -498,7 +502,7 @@ function classifyToolExecution(toolName, toolArgs = {}, result, errorMessage = '
|
|
|
498
502
|
? 'command'
|
|
499
503
|
: name.includes('skill')
|
|
500
504
|
? 'skills'
|
|
501
|
-
: name.includes('scheduled_task') || name === 'schedule_run'
|
|
505
|
+
: name.includes('scheduled_task') || name === 'schedule_run' || name.includes('widget')
|
|
502
506
|
? 'scheduler'
|
|
503
507
|
: name === 'send_message' || name === 'make_call'
|
|
504
508
|
? 'messaging'
|
|
@@ -1966,6 +1970,7 @@ class AgentEngine {
|
|
|
1966
1970
|
source: options.source || null,
|
|
1967
1971
|
chatId: options.chatId || null,
|
|
1968
1972
|
taskId: options.taskId || null,
|
|
1973
|
+
widgetId: options.widgetId || null,
|
|
1969
1974
|
deliveryState: options.deliveryState || null,
|
|
1970
1975
|
allowMultipleProactiveMessages: options.allowMultipleProactiveMessages === true,
|
|
1971
1976
|
allowExternalSideEffects: options.allowExternalSideEffects === true,
|
|
@@ -2801,7 +2806,7 @@ class AgentEngine {
|
|
|
2801
2806
|
if (toolName === 'send_message') return 'messaging';
|
|
2802
2807
|
if (toolName === 'make_call') return 'messaging';
|
|
2803
2808
|
if (toolName.startsWith('mcp_') || toolName.includes('mcp')) return 'mcp';
|
|
2804
|
-
if (toolName.includes('scheduled_task') || toolName === 'schedule_run') return 'scheduler';
|
|
2809
|
+
if (toolName.includes('scheduled_task') || toolName === 'schedule_run' || toolName.includes('widget')) return 'scheduler';
|
|
2805
2810
|
if (toolName.includes('subagent')) return 'subagent';
|
|
2806
2811
|
if (toolName === 'think') return 'thinking';
|
|
2807
2812
|
return 'tool';
|
|
@@ -221,9 +221,34 @@ function compactToolResult(toolName, toolArgs = {}, toolResult, options = {}) {
|
|
|
221
221
|
case 'schedule_run':
|
|
222
222
|
case 'delete_scheduled_task':
|
|
223
223
|
case 'update_scheduled_task':
|
|
224
|
+
case 'create_ai_widget':
|
|
225
|
+
case 'update_ai_widget':
|
|
226
|
+
case 'delete_ai_widget':
|
|
227
|
+
case 'save_widget_snapshot':
|
|
224
228
|
envelope = buildSimpleStatusEnvelope(toolName, toolResult, softLimit);
|
|
225
229
|
break;
|
|
226
230
|
|
|
231
|
+
case 'list_ai_widgets':
|
|
232
|
+
envelope = trimObject({
|
|
233
|
+
tool: toolName,
|
|
234
|
+
status: toolResult?.success === false || toolResult?.error ? 'error' : 'ok',
|
|
235
|
+
message: clampText(toolResult?.message || toolResult?.error || '', Math.floor(softLimit * 0.3)),
|
|
236
|
+
count: typeof toolResult?.count === 'number'
|
|
237
|
+
? toolResult.count
|
|
238
|
+
: (Array.isArray(toolResult?.widgets) ? toolResult.widgets.length : undefined),
|
|
239
|
+
widgets: Array.isArray(toolResult?.widgets)
|
|
240
|
+
? toolResult.widgets.slice(0, 6).map((widget) => trimObject({
|
|
241
|
+
id: widget?.id,
|
|
242
|
+
name: widget?.name,
|
|
243
|
+
template: widget?.template,
|
|
244
|
+
layoutVariant: widget?.layoutVariant,
|
|
245
|
+
refreshCron: widget?.refreshCron,
|
|
246
|
+
enabled: widget?.enabled,
|
|
247
|
+
}))
|
|
248
|
+
: undefined,
|
|
249
|
+
});
|
|
250
|
+
break;
|
|
251
|
+
|
|
227
252
|
case 'spawn_subagent':
|
|
228
253
|
envelope = trimObject({
|
|
229
254
|
tool: toolName,
|
|
@@ -936,6 +936,60 @@ function getAvailableTools(app, options = {}) {
|
|
|
936
936
|
required: ['task_id']
|
|
937
937
|
}
|
|
938
938
|
},
|
|
939
|
+
{
|
|
940
|
+
name: 'create_ai_widget',
|
|
941
|
+
description: 'Create an AI widget with a fixed template, approved layout variant, refresh cadence, and definition prompt. Cadence must be at least 1 hour.',
|
|
942
|
+
parameters: {
|
|
943
|
+
type: 'object',
|
|
944
|
+
properties: {
|
|
945
|
+
name: { type: 'string', description: 'Short widget name.' },
|
|
946
|
+
template: { type: 'string', enum: ['stat', 'summary', 'list'], description: 'Widget template family.' },
|
|
947
|
+
layout_variant: { type: 'string', description: 'Approved layout variant for the chosen template.' },
|
|
948
|
+
refresh_cron: { type: 'string', description: '5-field cron cadence. Never set faster than hourly.' },
|
|
949
|
+
prompt: { type: 'string', description: 'Self-contained definition of what the widget should track and how it should summarize it.' },
|
|
950
|
+
description: { type: 'string', description: 'Optional short operator-facing description.' },
|
|
951
|
+
system_hint: { type: 'string', description: 'Optional extra guidance for future refresh runs.' },
|
|
952
|
+
enabled: { type: 'boolean', description: 'Whether the widget should start enabled immediately.' },
|
|
953
|
+
run_initial_refresh: { type: 'boolean', description: 'When true, immediately run the first refresh after creation. Defaults to true.' }
|
|
954
|
+
},
|
|
955
|
+
required: ['name', 'template', 'layout_variant', 'refresh_cron', 'prompt']
|
|
956
|
+
}
|
|
957
|
+
},
|
|
958
|
+
{
|
|
959
|
+
name: 'list_ai_widgets',
|
|
960
|
+
description: 'List AI widgets for the current agent.',
|
|
961
|
+
parameters: { type: 'object', properties: {} }
|
|
962
|
+
},
|
|
963
|
+
{
|
|
964
|
+
name: 'update_ai_widget',
|
|
965
|
+
description: 'Update an existing AI widget. Use this when the user explicitly wants to change the widget definition, layout variant, cadence, or enabled state.',
|
|
966
|
+
parameters: {
|
|
967
|
+
type: 'object',
|
|
968
|
+
properties: {
|
|
969
|
+
widget_id: { type: 'string', description: 'Widget ID from list_ai_widgets.' },
|
|
970
|
+
name: { type: 'string', description: 'Updated widget name.' },
|
|
971
|
+
template: { type: 'string', enum: ['stat', 'summary', 'list'], description: 'Updated widget template.' },
|
|
972
|
+
layout_variant: { type: 'string', description: 'Approved layout variant for the chosen template.' },
|
|
973
|
+
refresh_cron: { type: 'string', description: 'Updated 5-field cron cadence. Never faster than hourly.' },
|
|
974
|
+
prompt: { type: 'string', description: 'Updated widget definition prompt.' },
|
|
975
|
+
description: { type: 'string', description: 'Optional updated operator-facing description.' },
|
|
976
|
+
system_hint: { type: 'string', description: 'Optional updated extra guidance for refresh runs.' },
|
|
977
|
+
enabled: { type: 'boolean', description: 'Enable or disable the widget.' }
|
|
978
|
+
},
|
|
979
|
+
required: ['widget_id']
|
|
980
|
+
}
|
|
981
|
+
},
|
|
982
|
+
{
|
|
983
|
+
name: 'delete_ai_widget',
|
|
984
|
+
description: 'Delete an AI widget by its ID.',
|
|
985
|
+
parameters: {
|
|
986
|
+
type: 'object',
|
|
987
|
+
properties: {
|
|
988
|
+
widget_id: { type: 'string', description: 'Widget ID from list_ai_widgets.' }
|
|
989
|
+
},
|
|
990
|
+
required: ['widget_id']
|
|
991
|
+
}
|
|
992
|
+
},
|
|
939
993
|
{
|
|
940
994
|
name: 'mcp_add_server',
|
|
941
995
|
description: 'Register and optionally start a new MCP (Model Context Protocol) server connection. Use this when the user asks to connect a new MCP server or when you discover a useful one. The server will appear in the MCP Servers page and its tools will be available to you immediately if auto_start is true.',
|
|
@@ -1097,6 +1151,23 @@ function getAvailableTools(app, options = {}) {
|
|
|
1097
1151
|
tools.push(...integrationTools);
|
|
1098
1152
|
}
|
|
1099
1153
|
|
|
1154
|
+
if (options.triggerSource === 'scheduler' && options.widgetId) {
|
|
1155
|
+
tools.push({
|
|
1156
|
+
name: 'save_widget_snapshot',
|
|
1157
|
+
description: 'Save the refreshed structured snapshot for the widget that is currently being updated. Call this exactly once per widget refresh run.',
|
|
1158
|
+
parameters: {
|
|
1159
|
+
type: 'object',
|
|
1160
|
+
properties: {
|
|
1161
|
+
snapshot: {
|
|
1162
|
+
type: 'object',
|
|
1163
|
+
description: 'Structured widget snapshot payload containing title, optional subtitle/body/metric/trend/rows/chips/iconToken/accentToken/updatedAt/deepLink.'
|
|
1164
|
+
}
|
|
1165
|
+
},
|
|
1166
|
+
required: ['snapshot']
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1100
1171
|
let visibleTools = tools;
|
|
1101
1172
|
if (options.userId != null) {
|
|
1102
1173
|
try {
|
|
@@ -1137,6 +1208,7 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
1137
1208
|
app,
|
|
1138
1209
|
triggerSource,
|
|
1139
1210
|
taskId,
|
|
1211
|
+
widgetId,
|
|
1140
1212
|
deliveryState = null,
|
|
1141
1213
|
allowMultipleProactiveMessages = false
|
|
1142
1214
|
} = context;
|
|
@@ -1176,6 +1248,7 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
1176
1248
|
const sk = () => app?.locals?.skillRunner || engine.skillRunner;
|
|
1177
1249
|
const sched = () => app?.locals?.scheduler || engine.scheduler;
|
|
1178
1250
|
const rec = () => app?.locals?.recordingManager || null;
|
|
1251
|
+
const widgets = () => app?.locals?.widgetService || null;
|
|
1179
1252
|
|
|
1180
1253
|
const integrationManager = integrations();
|
|
1181
1254
|
if (integrationManager) {
|
|
@@ -2224,6 +2297,115 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
2224
2297
|
}
|
|
2225
2298
|
}
|
|
2226
2299
|
|
|
2300
|
+
case 'create_ai_widget': {
|
|
2301
|
+
const widgetService = widgets();
|
|
2302
|
+
if (!widgetService) return { error: 'Widget service not available' };
|
|
2303
|
+
try {
|
|
2304
|
+
const widget = widgetService.createWidget(userId, {
|
|
2305
|
+
name: args.name,
|
|
2306
|
+
template: args.template,
|
|
2307
|
+
layoutVariant: args.layout_variant,
|
|
2308
|
+
refreshCron: args.refresh_cron,
|
|
2309
|
+
prompt: args.prompt,
|
|
2310
|
+
description: args.description,
|
|
2311
|
+
definition: {
|
|
2312
|
+
prompt: args.prompt,
|
|
2313
|
+
description: args.description,
|
|
2314
|
+
systemHint: args.system_hint,
|
|
2315
|
+
},
|
|
2316
|
+
enabled: args.enabled !== false,
|
|
2317
|
+
agentId,
|
|
2318
|
+
});
|
|
2319
|
+
let initialRefresh = null;
|
|
2320
|
+
if (args.run_initial_refresh !== false) {
|
|
2321
|
+
try {
|
|
2322
|
+
initialRefresh = await widgetService.refreshWidget(userId, widget.id, {
|
|
2323
|
+
taskId: widget.scheduledTaskId || null,
|
|
2324
|
+
});
|
|
2325
|
+
} catch (refreshErr) {
|
|
2326
|
+
initialRefresh = { error: refreshErr.message };
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
return {
|
|
2330
|
+
success: true,
|
|
2331
|
+
widget,
|
|
2332
|
+
initialRefresh,
|
|
2333
|
+
message: `AI widget "${widget.name}" created.`,
|
|
2334
|
+
};
|
|
2335
|
+
} catch (err) {
|
|
2336
|
+
return { error: err.message };
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
case 'list_ai_widgets': {
|
|
2341
|
+
const widgetService = widgets();
|
|
2342
|
+
if (!widgetService) return { error: 'Widget service not available' };
|
|
2343
|
+
const items = widgetService.listWidgets(userId, { agentId });
|
|
2344
|
+
return { widgets: items, count: items.length };
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
case 'update_ai_widget': {
|
|
2348
|
+
const widgetService = widgets();
|
|
2349
|
+
if (!widgetService) return { error: 'Widget service not available' };
|
|
2350
|
+
try {
|
|
2351
|
+
const existing = widgetService.getWidget(userId, args.widget_id);
|
|
2352
|
+
if (!existing || existing.agentId !== agentId) {
|
|
2353
|
+
return { error: 'Widget not found for this agent.' };
|
|
2354
|
+
}
|
|
2355
|
+
const widget = widgetService.updateWidget(userId, args.widget_id, {
|
|
2356
|
+
name: args.name,
|
|
2357
|
+
template: args.template,
|
|
2358
|
+
layoutVariant: args.layout_variant,
|
|
2359
|
+
refreshCron: args.refresh_cron,
|
|
2360
|
+
prompt: args.prompt,
|
|
2361
|
+
description: args.description,
|
|
2362
|
+
definition: args.prompt !== undefined || args.description !== undefined || args.system_hint !== undefined
|
|
2363
|
+
? {
|
|
2364
|
+
...(existing.definition || {}),
|
|
2365
|
+
...(args.prompt !== undefined ? { prompt: args.prompt } : {}),
|
|
2366
|
+
...(args.description !== undefined ? { description: args.description } : {}),
|
|
2367
|
+
...(args.system_hint !== undefined ? { systemHint: args.system_hint } : {}),
|
|
2368
|
+
}
|
|
2369
|
+
: undefined,
|
|
2370
|
+
enabled: args.enabled,
|
|
2371
|
+
agentId,
|
|
2372
|
+
});
|
|
2373
|
+
return { success: true, widget };
|
|
2374
|
+
} catch (err) {
|
|
2375
|
+
return { error: err.message };
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
case 'delete_ai_widget': {
|
|
2380
|
+
const widgetService = widgets();
|
|
2381
|
+
if (!widgetService) return { error: 'Widget service not available' };
|
|
2382
|
+
try {
|
|
2383
|
+
const existing = widgetService.getWidget(userId, args.widget_id);
|
|
2384
|
+
if (!existing || existing.agentId !== agentId) {
|
|
2385
|
+
return { error: 'Widget not found for this agent.' };
|
|
2386
|
+
}
|
|
2387
|
+
const deleted = widgetService.deleteWidget(userId, args.widget_id);
|
|
2388
|
+
return { success: true, ...deleted };
|
|
2389
|
+
} catch (err) {
|
|
2390
|
+
return { error: err.message };
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
case 'save_widget_snapshot': {
|
|
2395
|
+
const widgetService = widgets();
|
|
2396
|
+
if (!widgetService) return { error: 'Widget service not available' };
|
|
2397
|
+
if (!widgetId) return { error: 'save_widget_snapshot is only available during widget refresh runs.' };
|
|
2398
|
+
try {
|
|
2399
|
+
const snapshot = widgetService.saveSnapshot(userId, widgetId, args.snapshot, {
|
|
2400
|
+
sourceRunId: runId,
|
|
2401
|
+
status: 'ready',
|
|
2402
|
+
});
|
|
2403
|
+
return { success: true, snapshot };
|
|
2404
|
+
} catch (err) {
|
|
2405
|
+
return { error: err.message };
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2227
2409
|
case 'mcp_add_server': {
|
|
2228
2410
|
const mcpClient = mcp();
|
|
2229
2411
|
if (!mcpClient) return { error: 'MCP manager not available' };
|
|
@@ -11,6 +11,7 @@ const { SkillRunner } = require('./ai/toolRunner');
|
|
|
11
11
|
const { CommandRouter } = require('./commands/router');
|
|
12
12
|
const { MessagingManager } = require('./messaging/manager');
|
|
13
13
|
const { Scheduler } = require('./scheduler/cron');
|
|
14
|
+
const { WidgetService } = require('./widgets/service');
|
|
14
15
|
const { setupWebSocket } = require('./websocket');
|
|
15
16
|
const { registerMessagingAutomation } = require('./messaging/automation');
|
|
16
17
|
const { RecordingManager } = require('./recordings/manager');
|
|
@@ -417,6 +418,16 @@ function createRecordingManager(app, io) {
|
|
|
417
418
|
return recordingManager;
|
|
418
419
|
}
|
|
419
420
|
|
|
421
|
+
function createWidgetService(app) {
|
|
422
|
+
const widgetService = registerLocal(
|
|
423
|
+
app,
|
|
424
|
+
'widgetService',
|
|
425
|
+
new WidgetService({ app }),
|
|
426
|
+
);
|
|
427
|
+
logServiceReady('Widget service ready');
|
|
428
|
+
return widgetService;
|
|
429
|
+
}
|
|
430
|
+
|
|
420
431
|
function restoreMessagingConnections(messagingManager) {
|
|
421
432
|
void runBackgroundTask('[Messaging] Restore error:', () =>
|
|
422
433
|
messagingManager.restoreConnections(),
|
|
@@ -501,6 +512,7 @@ async function startServices(app, io) {
|
|
|
501
512
|
|
|
502
513
|
const messagingManager = createMessagingManager(app, io, agentEngine);
|
|
503
514
|
const recordingManager = createRecordingManager(app, io);
|
|
515
|
+
createWidgetService(app);
|
|
504
516
|
|
|
505
517
|
restoreMessagingConnections(messagingManager);
|
|
506
518
|
restoreMcpClients(mcpClient);
|
|
@@ -608,6 +620,25 @@ async function stopServices(app) {
|
|
|
608
620
|
);
|
|
609
621
|
}
|
|
610
622
|
|
|
623
|
+
if (app.locals.widgetService) {
|
|
624
|
+
const widgetService = app.locals.widgetService;
|
|
625
|
+
const cleanupMethod = ['shutdown', 'close', 'stop', 'dispose'].find(
|
|
626
|
+
(method) => typeof widgetService[method] === 'function',
|
|
627
|
+
);
|
|
628
|
+
if (cleanupMethod) {
|
|
629
|
+
tasks.push(
|
|
630
|
+
Promise.resolve()
|
|
631
|
+
.then(() => widgetService[cleanupMethod]())
|
|
632
|
+
.then(() => {
|
|
633
|
+
logServiceReady(`Widget service ${cleanupMethod} completed`);
|
|
634
|
+
})
|
|
635
|
+
.catch((err) => {
|
|
636
|
+
console.error('[Widget] Shutdown error:', getErrorMessage(err));
|
|
637
|
+
}),
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
611
642
|
if (app.locals.browserExtensionRegistry) {
|
|
612
643
|
try {
|
|
613
644
|
app.locals.browserExtensionRegistry.closeAll();
|
|
@@ -2,6 +2,7 @@ const cron = require('node-cron');
|
|
|
2
2
|
const crypto = require('crypto');
|
|
3
3
|
const db = require('../../db/database');
|
|
4
4
|
const { isMainAgent, resolveAgentId } = require('../agents/manager');
|
|
5
|
+
const { findNextRun } = require('./cron_utils');
|
|
5
6
|
|
|
6
7
|
const MAX_SCHEDULER_AUTONOMOUS_RETRIES = 1;
|
|
7
8
|
const MAX_RECURRING_TASK_START_DELAY_MS = 90 * 1000;
|
|
@@ -60,11 +61,27 @@ class Scheduler {
|
|
|
60
61
|
console.log('[Scheduler] One-time poller active (every 1 min)');
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
createTask(userId, {
|
|
64
|
+
createTask(userId, {
|
|
65
|
+
name,
|
|
66
|
+
cronExpression,
|
|
67
|
+
prompt,
|
|
68
|
+
enabled = true,
|
|
69
|
+
callTo = null,
|
|
70
|
+
callGreeting = null,
|
|
71
|
+
model = null,
|
|
72
|
+
runAt = null,
|
|
73
|
+
oneTime = false,
|
|
74
|
+
agentId = null,
|
|
75
|
+
taskType = 'agent_prompt',
|
|
76
|
+
taskConfig = null,
|
|
77
|
+
}) {
|
|
64
78
|
const scopedAgentId = resolveAgentId(userId, agentId);
|
|
65
79
|
const notifyTarget = this._getDefaultNotifyTarget(userId, scopedAgentId);
|
|
66
80
|
|
|
67
81
|
if (oneTime) {
|
|
82
|
+
if (taskType !== 'agent_prompt') {
|
|
83
|
+
throw new Error('One-time runs support only agent_prompt tasks.');
|
|
84
|
+
}
|
|
68
85
|
if (!runAt) throw new Error('runAt is required for one-time tasks');
|
|
69
86
|
const runAtDate = new Date(runAt);
|
|
70
87
|
if (isNaN(runAtDate.getTime())) throw new Error(`Invalid runAt value: ${runAt}`);
|
|
@@ -88,17 +105,25 @@ class Scheduler {
|
|
|
88
105
|
throw new Error(`Invalid cron expression: ${cronExpression}`);
|
|
89
106
|
}
|
|
90
107
|
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
108
|
+
let config;
|
|
109
|
+
if (taskType === 'widget_refresh') {
|
|
110
|
+
config = this._normalizeTaskConfig(taskConfig || {});
|
|
111
|
+
if (!config.widgetId) {
|
|
112
|
+
throw new Error('widget_refresh tasks require widgetId.');
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
config = { prompt };
|
|
116
|
+
if (callTo) { config.callTo = callTo; config.callGreeting = callGreeting || ''; }
|
|
117
|
+
if (typeof model === 'string' && model.trim()) config.model = model.trim();
|
|
118
|
+
if (notifyTarget.platform && notifyTarget.to) {
|
|
119
|
+
config.notifyPlatform = notifyTarget.platform;
|
|
120
|
+
config.notifyTo = notifyTarget.to;
|
|
121
|
+
}
|
|
97
122
|
}
|
|
98
123
|
|
|
99
124
|
const result = db.prepare(
|
|
100
125
|
'INSERT INTO scheduled_tasks (user_id, agent_id, name, cron_expression, task_type, task_config, enabled) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
|
101
|
-
).run(userId, scopedAgentId, name, cronExpression,
|
|
126
|
+
).run(userId, scopedAgentId, name, cronExpression, taskType, JSON.stringify(config), enabled ? 1 : 0);
|
|
102
127
|
|
|
103
128
|
const taskId = result.lastInsertRowid;
|
|
104
129
|
|
|
@@ -106,12 +131,15 @@ class Scheduler {
|
|
|
106
131
|
this._scheduleTask(taskId, userId, cronExpression, config, scopedAgentId);
|
|
107
132
|
}
|
|
108
133
|
|
|
109
|
-
return { id: taskId, name, cronExpression, enabled, callTo: config.callTo || null, model: config.model || null, agentId: scopedAgentId };
|
|
134
|
+
return { id: taskId, name, cronExpression, enabled, callTo: config.callTo || null, model: config.model || null, agentId: scopedAgentId, taskType, widgetId: config.widgetId || null };
|
|
110
135
|
}
|
|
111
136
|
|
|
112
|
-
updateTask(taskId, userId, updates) {
|
|
137
|
+
updateTask(taskId, userId, updates, options = {}) {
|
|
113
138
|
const task = db.prepare('SELECT * FROM scheduled_tasks WHERE id = ? AND user_id = ?').get(taskId, userId);
|
|
114
139
|
if (!task) throw new Error('Task not found');
|
|
140
|
+
if (task.task_type === 'widget_refresh' && options.allowManaged !== true) {
|
|
141
|
+
throw new Error('Widget refresh tasks must be updated via widgets.');
|
|
142
|
+
}
|
|
115
143
|
|
|
116
144
|
const name = updates.name || task.name;
|
|
117
145
|
const cronExpr = updates.cronExpression || task.cron_expression;
|
|
@@ -122,25 +150,34 @@ class Scheduler {
|
|
|
122
150
|
|
|
123
151
|
// Merge config — start from existing, apply any changes
|
|
124
152
|
let config = this._normalizeTaskConfig(task.task_config);
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (updates.model !== undefined) {
|
|
129
|
-
if (typeof updates.model === 'string' && updates.model.trim()) {
|
|
130
|
-
config.model = updates.model.trim();
|
|
131
|
-
} else {
|
|
132
|
-
delete config.model;
|
|
153
|
+
if (task.task_type === 'widget_refresh') {
|
|
154
|
+
if (updates.taskConfig !== undefined) {
|
|
155
|
+
config = this._normalizeTaskConfig(updates.taskConfig);
|
|
133
156
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
157
|
+
if (!config.widgetId) {
|
|
158
|
+
throw new Error('widget_refresh tasks require widgetId.');
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
if (updates.prompt !== undefined) config.prompt = updates.prompt;
|
|
162
|
+
if (updates.callTo !== undefined) config.callTo = updates.callTo || null;
|
|
163
|
+
if (updates.callGreeting !== undefined) config.callGreeting = updates.callGreeting || null;
|
|
164
|
+
if (updates.model !== undefined) {
|
|
165
|
+
if (typeof updates.model === 'string' && updates.model.trim()) {
|
|
166
|
+
config.model = updates.model.trim();
|
|
167
|
+
} else {
|
|
168
|
+
delete config.model;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (!config.notifyPlatform || !config.notifyTo) {
|
|
172
|
+
const notifyTarget = this._getDefaultNotifyTarget(userId, agentId);
|
|
173
|
+
if (notifyTarget.platform && notifyTarget.to) {
|
|
174
|
+
config.notifyPlatform = notifyTarget.platform;
|
|
175
|
+
config.notifyTo = notifyTarget.to;
|
|
176
|
+
}
|
|
140
177
|
}
|
|
178
|
+
// Clean up nulls
|
|
179
|
+
if (!config.callTo) { delete config.callTo; delete config.callGreeting; }
|
|
141
180
|
}
|
|
142
|
-
// Clean up nulls
|
|
143
|
-
if (!config.callTo) { delete config.callTo; delete config.callGreeting; }
|
|
144
181
|
|
|
145
182
|
if (updates.cronExpression && !cron.validate(updates.cronExpression)) {
|
|
146
183
|
throw new Error(`Invalid cron expression: ${updates.cronExpression}`);
|
|
@@ -160,12 +197,15 @@ class Scheduler {
|
|
|
160
197
|
this._scheduleTask(taskId, userId, cronExpr, config, agentId);
|
|
161
198
|
}
|
|
162
199
|
|
|
163
|
-
return { id: taskId, name, cronExpression: cronExpr, enabled, callTo: config.callTo || null, model: config.model || null, agentId };
|
|
200
|
+
return { id: taskId, name, cronExpression: cronExpr, enabled, callTo: config.callTo || null, model: config.model || null, agentId, taskType: task.task_type || 'agent_prompt', widgetId: config.widgetId || null };
|
|
164
201
|
}
|
|
165
202
|
|
|
166
|
-
deleteTask(taskId, userId) {
|
|
203
|
+
deleteTask(taskId, userId, options = {}) {
|
|
167
204
|
const task = db.prepare('SELECT * FROM scheduled_tasks WHERE id = ? AND user_id = ?').get(taskId, userId);
|
|
168
205
|
if (!task) throw new Error('Task not found');
|
|
206
|
+
if (task.task_type === 'widget_refresh' && options.allowManaged !== true) {
|
|
207
|
+
throw new Error('Widget refresh tasks must be deleted via widgets.');
|
|
208
|
+
}
|
|
169
209
|
|
|
170
210
|
const existing = this.jobs.get(taskId);
|
|
171
211
|
if (existing) {
|
|
@@ -203,6 +243,8 @@ class Scheduler {
|
|
|
203
243
|
enabled: !!t.enabled,
|
|
204
244
|
lastRun: t.last_run,
|
|
205
245
|
nextRun: t.one_time ? t.run_at : this._getNextRun(t.cron_expression),
|
|
246
|
+
taskType: t.task_type || 'agent_prompt',
|
|
247
|
+
widgetId: config.widgetId || null,
|
|
206
248
|
config,
|
|
207
249
|
prompt: config.prompt || '',
|
|
208
250
|
model: config.model || null,
|
|
@@ -308,6 +350,20 @@ class Scheduler {
|
|
|
308
350
|
this.io.to(`user:${userId}`).emit('scheduler:task_running', { taskId, timestamp: new Date().toISOString() });
|
|
309
351
|
|
|
310
352
|
try {
|
|
353
|
+
if (task.task_type === 'widget_refresh') {
|
|
354
|
+
const widgetService = this.app?.locals?.widgetService;
|
|
355
|
+
if (!widgetService || !config.widgetId) {
|
|
356
|
+
throw new Error('Widget refresh task is missing widget context.');
|
|
357
|
+
}
|
|
358
|
+
const result = await widgetService.refreshWidget(userId, config.widgetId, {
|
|
359
|
+
taskId,
|
|
360
|
+
manual: executionMeta.manual === true,
|
|
361
|
+
scheduledAt: executionMeta.scheduledAt || null,
|
|
362
|
+
});
|
|
363
|
+
this.io.to(`user:${userId}`).emit('scheduler:task_complete', { taskId, result });
|
|
364
|
+
return result;
|
|
365
|
+
}
|
|
366
|
+
|
|
311
367
|
if (this.agentEngine && config.prompt !== undefined) {
|
|
312
368
|
let notifyHint = '';
|
|
313
369
|
|
|
@@ -458,10 +514,7 @@ class Scheduler {
|
|
|
458
514
|
|
|
459
515
|
_getNextRun(cronExpression) {
|
|
460
516
|
try {
|
|
461
|
-
|
|
462
|
-
interval.stop();
|
|
463
|
-
// node-cron doesn't expose nextRun; we just return null
|
|
464
|
-
return null;
|
|
517
|
+
return findNextRun(cronExpression)?.toISOString() || null;
|
|
465
518
|
} catch {
|
|
466
519
|
return null;
|
|
467
520
|
}
|