neoagent 2.3.1-beta.2 → 2.3.1-beta.3
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/README.md +2 -0
- package/package.json +2 -1
- package/server/db/database.js +57 -0
- package/server/http/routes/screenHistory.js +46 -0
- package/server/http/routes/triggers.js +81 -0
- package/server/http/routes.js +3 -1
- package/server/public/assets/NOTICES +61 -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 +42896 -42725
- package/server/services/ai/systemPrompt.js +1 -1
- package/server/services/ai/tools.js +35 -6
- package/server/services/browser/controller.js +47 -3
- package/server/services/desktop/screenRecorder.js +126 -0
- package/server/services/integrations/whatsapp/provider.js +6 -2
- package/server/services/manager.js +22 -0
- package/server/services/skills/base_catalog.js +33 -0
- package/server/services/tasks/adapters/index.js +1 -0
- package/server/services/tasks/adapters/manual.js +12 -0
- package/server/services/tasks/runtime.js +1 -1
- package/server/services/widgets/service.js +49 -4
|
@@ -138,7 +138,7 @@ When drafting on behalf of the user, match their likely voice from available con
|
|
|
138
138
|
If the user approves a previously shown draft, send that draft rather than silently rewriting it.
|
|
139
139
|
|
|
140
140
|
TASKS
|
|
141
|
-
Use one-time schedule triggers for single reminders or delayed actions, recurring schedule triggers for repeating automation, and official integration triggers when the task should react to connected Gmail, Outlook, Slack, Teams, or WhatsApp Personal events. When calling task tools, prefer one unified trigger section: trigger={ type, config }. Make task prompts self-contained: who/what to check, exact action to take, when to notify, and which channel to use if known.
|
|
141
|
+
Use manual triggers for run-on-demand tasks, one-time schedule triggers for single reminders or delayed actions, recurring schedule triggers for repeating automation, and official integration triggers when the task should react to connected Gmail, Outlook, Slack, Teams, or WhatsApp Personal events. When calling task tools, prefer one unified trigger section: trigger={ type, config }. Make task prompts self-contained: who/what to check, exact action to take, when to notify, and which channel to use if known.
|
|
142
142
|
Do not create vague tasks like "check this" when the future run would not know what "this" means. Resolve references into names, links, file paths, IDs, dates, and success criteria before saving the task.
|
|
143
143
|
For notification tasks, distinguish between notifying the user in their current messaging channel, emailing the user, and contacting someone else. Default reminders should notify the user through the active messaging channel unless the user explicitly asks for email, phone, or a third party.
|
|
144
144
|
When creating or updating a task, include whether it should notify every time, only on change, only on errors, or only when a condition is met. If unspecified, choose the least noisy useful behavior and say what you chose.
|
|
@@ -973,8 +973,8 @@ function getAvailableTools(app, options = {}) {
|
|
|
973
973
|
type: 'object',
|
|
974
974
|
properties: {
|
|
975
975
|
name: { type: 'string', description: 'Short descriptive name for the task.' },
|
|
976
|
-
trigger: { type: 'object', description: 'Unified trigger object. Prefer { type: "schedule" | integration_trigger_type, config: {...} }.' },
|
|
977
|
-
trigger_type: { type: 'string', description: 'Trigger type such as schedule, gmail_message_received, outlook_email_received, slack_message_received, teams_message_received, weather_event, or whatsapp_personal_message_received.' },
|
|
976
|
+
trigger: { type: 'object', description: 'Unified trigger object. Prefer { type: "manual" | "schedule" | integration_trigger_type, config: {...} }.' },
|
|
977
|
+
trigger_type: { type: 'string', description: 'Trigger type such as manual, schedule, gmail_message_received, outlook_email_received, slack_message_received, teams_message_received, weather_event, or whatsapp_personal_message_received.' },
|
|
978
978
|
trigger_config: { type: 'object', description: 'Trigger-specific configuration object. For schedule triggers prefer { mode: "recurring", cronExpression: "m h dom mon dow" } or { mode: "one_time", runAt: ISO datetime }. 5-field cron only (seconds unsupported).' },
|
|
979
979
|
prompt: { type: 'string', description: 'The instructions the agent will run when the trigger fires.' },
|
|
980
980
|
enabled: { type: 'boolean', description: 'Whether to activate immediately.' },
|
|
@@ -1010,7 +1010,7 @@ function getAvailableTools(app, options = {}) {
|
|
|
1010
1010
|
task_id: { type: 'number', description: 'The numeric ID of the task to update.' },
|
|
1011
1011
|
name: { type: 'string', description: 'New name for the task.' },
|
|
1012
1012
|
trigger: { type: 'object', description: 'Unified trigger object. Use { type, config } to update trigger in one section.' },
|
|
1013
|
-
trigger_type: { type: 'string', description: 'Updated trigger type.' },
|
|
1013
|
+
trigger_type: { type: 'string', description: 'Updated trigger type, e.g. manual, schedule, or integration trigger type.' },
|
|
1014
1014
|
trigger_config: { type: 'object', description: 'Updated trigger-specific configuration. For schedule triggers use mode+cronExpression (recurring) or mode+runAt (one_time).' },
|
|
1015
1015
|
prompt: { type: 'string', description: 'Updated task prompt.' },
|
|
1016
1016
|
enabled: { type: 'boolean', description: 'Enable or disable the task.' },
|
|
@@ -1152,6 +1152,17 @@ function getAvailableTools(app, options = {}) {
|
|
|
1152
1152
|
required: ['image_path']
|
|
1153
1153
|
}
|
|
1154
1154
|
},
|
|
1155
|
+
{
|
|
1156
|
+
name: 'ocr_extract',
|
|
1157
|
+
description: 'Extract raw text from an image locally using Tesseract OCR. This is faster and completely offline compared to analyze_image.',
|
|
1158
|
+
parameters: {
|
|
1159
|
+
type: 'object',
|
|
1160
|
+
properties: {
|
|
1161
|
+
image_path: { type: 'string', description: 'Absolute path to the image file' }
|
|
1162
|
+
},
|
|
1163
|
+
required: ['image_path']
|
|
1164
|
+
}
|
|
1165
|
+
},
|
|
1155
1166
|
{
|
|
1156
1167
|
name: 'read_health_data',
|
|
1157
1168
|
description: 'Read the user\'s synced mobile health data. Omit metric_type for a summary of all available metrics. With metric_type, returns an aggregate summary (total, avg, min, max over all stored data) plus the most recent individual records. Always report the summary figures — avoid listing every raw record.',
|
|
@@ -2311,13 +2322,17 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
2311
2322
|
if (!resolvedTrigger.hasType || !resolvedTrigger.triggerType) {
|
|
2312
2323
|
return { error: 'Task trigger type is required (use trigger.type or trigger_type).' };
|
|
2313
2324
|
}
|
|
2314
|
-
|
|
2325
|
+
const normalizedTriggerType = String(resolvedTrigger.triggerType || '').trim();
|
|
2326
|
+
const triggerConfig = (!resolvedTrigger.hasConfig || resolvedTrigger.triggerConfig === undefined)
|
|
2327
|
+
? (normalizedTriggerType === 'manual' ? {} : undefined)
|
|
2328
|
+
: resolvedTrigger.triggerConfig;
|
|
2329
|
+
if (triggerConfig === undefined) {
|
|
2315
2330
|
return { error: 'Task trigger config is required (use trigger.config or trigger_config).' };
|
|
2316
2331
|
}
|
|
2317
2332
|
const task = await s.createTask(userId, {
|
|
2318
2333
|
name: args.name,
|
|
2319
|
-
triggerType:
|
|
2320
|
-
triggerConfig
|
|
2334
|
+
triggerType: normalizedTriggerType,
|
|
2335
|
+
triggerConfig,
|
|
2321
2336
|
prompt: args.prompt,
|
|
2322
2337
|
enabled: args.enabled !== false,
|
|
2323
2338
|
model: args.model || null,
|
|
@@ -2580,6 +2595,20 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
2580
2595
|
}
|
|
2581
2596
|
}
|
|
2582
2597
|
|
|
2598
|
+
case 'ocr_extract': {
|
|
2599
|
+
try {
|
|
2600
|
+
const fs = require('fs');
|
|
2601
|
+
if (!fs.existsSync(args.image_path)) {
|
|
2602
|
+
return { error: 'File not found: ' + args.image_path };
|
|
2603
|
+
}
|
|
2604
|
+
const Tesseract = require('tesseract.js');
|
|
2605
|
+
const result = await Tesseract.recognize(args.image_path, 'eng');
|
|
2606
|
+
return { text: result.data.text, confidence: result.data.confidence };
|
|
2607
|
+
} catch (err) {
|
|
2608
|
+
return { error: err.message };
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2583
2612
|
case 'spawn_subagent': {
|
|
2584
2613
|
try {
|
|
2585
2614
|
const task = args.context ? `${args.task}\n\nContext: ${args.context}` : args.task;
|
|
@@ -6,11 +6,11 @@ const SCREENSHOTS_DIR = path.join(DATA_DIR, 'screenshots');
|
|
|
6
6
|
if (!fs.existsSync(SCREENSHOTS_DIR)) fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
|
|
7
7
|
|
|
8
8
|
const USER_AGENTS = [
|
|
9
|
-
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/
|
|
10
|
-
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/
|
|
9
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
|
|
10
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
|
|
11
11
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
|
12
12
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
|
13
|
-
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/
|
|
13
|
+
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
|
|
14
14
|
];
|
|
15
15
|
|
|
16
16
|
const VIEWPORTS = [
|
|
@@ -148,6 +148,50 @@ class BrowserController {
|
|
|
148
148
|
return arr;
|
|
149
149
|
}
|
|
150
150
|
});
|
|
151
|
+
|
|
152
|
+
// WebGL Spoofing
|
|
153
|
+
const getParameterProxyHandler = {
|
|
154
|
+
apply: function(target, ctx, args) {
|
|
155
|
+
const param = args[0];
|
|
156
|
+
// UNMASKED_VENDOR_WEBGL
|
|
157
|
+
if (param === 37445) return 'Google Inc. (Apple)';
|
|
158
|
+
// UNMASKED_RENDERER_WEBGL
|
|
159
|
+
if (param === 37446) return 'ANGLE (Apple, Apple M2, OpenGL 4.1)';
|
|
160
|
+
return Reflect.apply(target, ctx, args);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
const getParam = WebGLRenderingContext.prototype.getParameter;
|
|
164
|
+
WebGLRenderingContext.prototype.getParameter = new Proxy(getParam, getParameterProxyHandler);
|
|
165
|
+
if (typeof WebGL2RenderingContext !== 'undefined') {
|
|
166
|
+
const getParam2 = WebGL2RenderingContext.prototype.getParameter;
|
|
167
|
+
WebGL2RenderingContext.prototype.getParameter = new Proxy(getParam2, getParameterProxyHandler);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Canvas Spoofing (slight noise)
|
|
171
|
+
const originalFillText = CanvasRenderingContext2D.prototype.fillText;
|
|
172
|
+
CanvasRenderingContext2D.prototype.fillText = function(...args) {
|
|
173
|
+
if (!this._spoofing_applied) {
|
|
174
|
+
this._spoofing_applied = true;
|
|
175
|
+
const r = Math.random() * 0.0001;
|
|
176
|
+
const g = Math.random() * 0.0001;
|
|
177
|
+
const b = Math.random() * 0.0001;
|
|
178
|
+
this.fillStyle = \`rgba(\${Math.floor(r * 255)}, \${Math.floor(g * 255)}, \${Math.floor(b * 255)}, 0.01)\`;
|
|
179
|
+
originalFillText.call(this, "spoof", 0, 0);
|
|
180
|
+
}
|
|
181
|
+
return originalFillText.apply(this, args);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Media Devices Spoofing
|
|
185
|
+
if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
|
|
186
|
+
const originalEnumerateDevices = navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices);
|
|
187
|
+
navigator.mediaDevices.enumerateDevices = async () => {
|
|
188
|
+
return [
|
|
189
|
+
{ kind: 'audioinput', deviceId: 'default', groupId: 'a', label: 'MacBook Pro Microphone' },
|
|
190
|
+
{ kind: 'audiooutput', deviceId: 'default', groupId: 'b', label: 'MacBook Pro Speakers' },
|
|
191
|
+
{ kind: 'videoinput', deviceId: 'default', groupId: 'c', label: 'FaceTime HD Camera' }
|
|
192
|
+
];
|
|
193
|
+
};
|
|
194
|
+
}
|
|
151
195
|
})();
|
|
152
196
|
`);
|
|
153
197
|
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs/promises');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const { exec } = require('child_process');
|
|
7
|
+
const { promisify } = require('util');
|
|
8
|
+
const tesseract = require('tesseract.js');
|
|
9
|
+
const db = require('../../db/database');
|
|
10
|
+
const { getErrorMessage } = require('../bootstrap_helpers');
|
|
11
|
+
|
|
12
|
+
const execAsync = promisify(exec);
|
|
13
|
+
|
|
14
|
+
class ScreenRecorder {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.intervalMs = 10000; // 10 seconds
|
|
17
|
+
this.intervalId = null;
|
|
18
|
+
this.cleanupIntervalId = null;
|
|
19
|
+
this.isRecording = false;
|
|
20
|
+
this.isProcessing = false;
|
|
21
|
+
this.tempFilePath = path.join(os.tmpdir(), `neoagent-screen-${Date.now()}.png`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
start() {
|
|
25
|
+
if (process.platform !== 'darwin') {
|
|
26
|
+
console.log('[ScreenRecorder] Not starting: Screen recording is currently macOS only.');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (this.isRecording) return;
|
|
31
|
+
this.isRecording = true;
|
|
32
|
+
|
|
33
|
+
console.log('[ScreenRecorder] Starting continuous screen recording (10s interval)');
|
|
34
|
+
|
|
35
|
+
// Start the recording loop
|
|
36
|
+
this.intervalId = setInterval(() => this.captureAndProcess(), this.intervalMs);
|
|
37
|
+
|
|
38
|
+
// Run an initial capture
|
|
39
|
+
this.captureAndProcess();
|
|
40
|
+
|
|
41
|
+
// Start daily cleanup of old records (7 days)
|
|
42
|
+
this.cleanupIntervalId = setInterval(() => this.cleanupOldRecords(), 24 * 60 * 60 * 1000);
|
|
43
|
+
this.cleanupOldRecords();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
stop() {
|
|
47
|
+
this.isRecording = false;
|
|
48
|
+
if (this.intervalId) {
|
|
49
|
+
clearInterval(this.intervalId);
|
|
50
|
+
this.intervalId = null;
|
|
51
|
+
}
|
|
52
|
+
if (this.cleanupIntervalId) {
|
|
53
|
+
clearInterval(this.cleanupIntervalId);
|
|
54
|
+
this.cleanupIntervalId = null;
|
|
55
|
+
}
|
|
56
|
+
console.log('[ScreenRecorder] Stopped continuous screen recording');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async captureAndProcess() {
|
|
60
|
+
if (this.isProcessing || !this.isRecording) return;
|
|
61
|
+
this.isProcessing = true;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
// Capture screen silently (-x) to file
|
|
65
|
+
await execAsync(`screencapture -x "${this.tempFilePath}"`);
|
|
66
|
+
|
|
67
|
+
// Verify file exists
|
|
68
|
+
await fs.access(this.tempFilePath);
|
|
69
|
+
|
|
70
|
+
// Extract text via local OCR
|
|
71
|
+
const { data } = await tesseract.recognize(this.tempFilePath, 'eng+deu', {
|
|
72
|
+
logger: () => {} // Silence verbose OCR logs
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const textContent = data.text.trim();
|
|
76
|
+
|
|
77
|
+
// Only store if meaningful text was found
|
|
78
|
+
if (textContent.length > 5) {
|
|
79
|
+
// We need a user ID. For the local desktop agent, usually user 1 or we query the active user.
|
|
80
|
+
const userRow = db.prepare('SELECT id FROM users ORDER BY id ASC LIMIT 1').get();
|
|
81
|
+
if (userRow) {
|
|
82
|
+
// Identify the active foreground app via AppleScript
|
|
83
|
+
let appName = 'Unknown';
|
|
84
|
+
try {
|
|
85
|
+
const { stdout } = await execAsync(`osascript -e 'tell application "System Events" to get name of first application process whose frontmost is true'`);
|
|
86
|
+
appName = stdout.trim();
|
|
87
|
+
} catch (e) {
|
|
88
|
+
// Ignore AppleScript errors
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
db.prepare(\`
|
|
92
|
+
INSERT INTO screen_history (user_id, app_name, text_content)
|
|
93
|
+
VALUES (?, ?, ?)
|
|
94
|
+
\`).run(userRow.id, appName, textContent);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error('[ScreenRecorder] Capture/OCR failed:', getErrorMessage(err));
|
|
100
|
+
} finally {
|
|
101
|
+
// Always cleanup the screenshot image immediately
|
|
102
|
+
try {
|
|
103
|
+
await fs.unlink(this.tempFilePath);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
// Ignore unlink errors if file didn't exist
|
|
106
|
+
}
|
|
107
|
+
this.isProcessing = false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
cleanupOldRecords() {
|
|
112
|
+
try {
|
|
113
|
+
const result = db.prepare(\`
|
|
114
|
+
DELETE FROM screen_history
|
|
115
|
+
WHERE timestamp < datetime('now', '-7 days')
|
|
116
|
+
\`).run();
|
|
117
|
+
if (result.changes > 0) {
|
|
118
|
+
console.log(\`[ScreenRecorder] Purged \${result.changes} old screen history records.\`);
|
|
119
|
+
}
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error('[ScreenRecorder] Cleanup failed:', getErrorMessage(err));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = { ScreenRecorder };
|
|
@@ -545,7 +545,7 @@ class WhatsAppPersonalProvider extends EventEmitter {
|
|
|
545
545
|
chatCount: client.chats.size,
|
|
546
546
|
cachedMessageChats: client.messages.size,
|
|
547
547
|
syncNote:
|
|
548
|
-
'Read tools only
|
|
548
|
+
'This account is linked and connected. Read tools expose only chats and messages currently available in this session cache.',
|
|
549
549
|
},
|
|
550
550
|
};
|
|
551
551
|
case 'whatsapp_personal_list_chats': {
|
|
@@ -557,10 +557,14 @@ class WhatsAppPersonalProvider extends EventEmitter {
|
|
|
557
557
|
.slice(0, limit);
|
|
558
558
|
return {
|
|
559
559
|
result: {
|
|
560
|
+
account: client.accountEmail || connection.account_email || null,
|
|
561
|
+
connected: client.status === 'connected',
|
|
560
562
|
chats,
|
|
561
563
|
count: chats.length,
|
|
562
564
|
syncNote:
|
|
563
|
-
|
|
565
|
+
chats.length > 0
|
|
566
|
+
? 'Chats shown here come from the current personal WhatsApp cache.'
|
|
567
|
+
: 'The personal WhatsApp account is connected, but no chats are cached yet. The cache populates as history/events are synchronized.',
|
|
564
568
|
},
|
|
565
569
|
};
|
|
566
570
|
}
|
|
@@ -24,6 +24,7 @@ const { RuntimeManager } = require('./runtime/manager');
|
|
|
24
24
|
const { BrowserExtensionRegistry } = require('./browser/extension/registry');
|
|
25
25
|
const { DesktopCompanionRegistry } = require('./desktop/registry');
|
|
26
26
|
const { DesktopProvider } = require('./desktop/provider');
|
|
27
|
+
const { ScreenRecorder } = require('./desktop/screenRecorder');
|
|
27
28
|
const { assertRuntimeValidation, getRuntimeValidation } = require('./runtime/validation');
|
|
28
29
|
const {
|
|
29
30
|
getErrorMessage,
|
|
@@ -428,6 +429,17 @@ function createWidgetService(app) {
|
|
|
428
429
|
return widgetService;
|
|
429
430
|
}
|
|
430
431
|
|
|
432
|
+
function createScreenRecorder(app) {
|
|
433
|
+
const screenRecorder = registerLocal(
|
|
434
|
+
app,
|
|
435
|
+
'screenRecorder',
|
|
436
|
+
new ScreenRecorder(),
|
|
437
|
+
);
|
|
438
|
+
screenRecorder.start();
|
|
439
|
+
logServiceReady('Screen recorder started');
|
|
440
|
+
return screenRecorder;
|
|
441
|
+
}
|
|
442
|
+
|
|
431
443
|
function restoreMessagingConnections(messagingManager) {
|
|
432
444
|
void runBackgroundTask('[Messaging] Restore error:', () =>
|
|
433
445
|
messagingManager.restoreConnections(),
|
|
@@ -513,6 +525,7 @@ async function startServices(app, io) {
|
|
|
513
525
|
const messagingManager = createMessagingManager(app, io, agentEngine);
|
|
514
526
|
const recordingManager = createRecordingManager(app, io);
|
|
515
527
|
createWidgetService(app);
|
|
528
|
+
createScreenRecorder(app);
|
|
516
529
|
|
|
517
530
|
restoreMessagingConnections(messagingManager);
|
|
518
531
|
restoreMcpClients(mcpClient);
|
|
@@ -639,6 +652,15 @@ async function stopServices(app) {
|
|
|
639
652
|
}
|
|
640
653
|
}
|
|
641
654
|
|
|
655
|
+
if (app.locals.screenRecorder) {
|
|
656
|
+
try {
|
|
657
|
+
app.locals.screenRecorder.stop();
|
|
658
|
+
logServiceReady('Screen recorder stopped');
|
|
659
|
+
} catch (err) {
|
|
660
|
+
console.error('[ScreenRecorder] Stop error:', getErrorMessage(err));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
642
664
|
if (app.locals.browserExtensionRegistry) {
|
|
643
665
|
try {
|
|
644
666
|
app.locals.browserExtensionRegistry.closeAll();
|
|
@@ -1161,6 +1161,39 @@ Auto-orients and arranges the STL, applies settings from JSON files, slices all
|
|
|
1161
1161
|
3. Build the command from the flags above
|
|
1162
1162
|
4. Run it and check for errors in the output (debug level 2 is a good default)
|
|
1163
1163
|
5. Report the output file location and any warnings`
|
|
1164
|
+
},
|
|
1165
|
+
|
|
1166
|
+
{
|
|
1167
|
+
id: 'package-tracker',
|
|
1168
|
+
name: 'Package Tracker',
|
|
1169
|
+
description: 'Track packages by automatically detecting the carrier and fetching the tracking link.',
|
|
1170
|
+
category: 'info',
|
|
1171
|
+
icon: '📦',
|
|
1172
|
+
content: `---
|
|
1173
|
+
name: package-tracker
|
|
1174
|
+
description: Track packages by automatically detecting the carrier and fetching the tracking link.
|
|
1175
|
+
category: info
|
|
1176
|
+
icon: 📦
|
|
1177
|
+
enabled: true
|
|
1178
|
+
---
|
|
1179
|
+
|
|
1180
|
+
You are a package tracking assistant. When the user gives you a tracking number, you should:
|
|
1181
|
+
|
|
1182
|
+
1. Identify the carrier using common regex patterns:
|
|
1183
|
+
- UPS: \`\\b(1Z[0-9A-Z]{16})\\b\`
|
|
1184
|
+
- FedEx: \`\\b([0-9]{12,15})\\b\` (usually 12 or 15 digits)
|
|
1185
|
+
- USPS: \`\\b(94[0-9]{20})\\b\` or \`\\b([A-Z]{2}[0-9]{9}US)\\b\`
|
|
1186
|
+
- DHL: \`\\b([0-9]{10})\\b\` or \`\\b([0-9]{20})\\b\`
|
|
1187
|
+
|
|
1188
|
+
2. Generate the direct tracking link for the user:
|
|
1189
|
+
- UPS: \`https://www.ups.com/track?tracknum={number}\`
|
|
1190
|
+
- FedEx: \`https://www.fedex.com/fedextrack/?trknbr={number}\`
|
|
1191
|
+
- USPS: \`https://tools.usps.com/go/TrackConfirmAction?tLabels={number}\`
|
|
1192
|
+
- DHL: \`https://www.dhl.com/global-en/home/tracking/tracking-express.html?submit=1&tracking-id={number}\`
|
|
1193
|
+
|
|
1194
|
+
3. If the carrier is not obvious, use \`web_search\` with the query "track package {number}" to identify the carrier.
|
|
1195
|
+
4. Present the carrier and the direct tracking link to the user clearly.
|
|
1196
|
+
5. Optionally use \`browser_navigate\` or \`http_request\` to fetch the current status from the tracking link, but note that many carriers block automated scraping. The direct link is the most important output.`
|
|
1164
1197
|
}
|
|
1165
1198
|
];
|
|
1166
1199
|
|
|
@@ -66,7 +66,7 @@ class TaskRuntime {
|
|
|
66
66
|
label: adapter.label,
|
|
67
67
|
providerKey: adapter.providerKey || null,
|
|
68
68
|
appKey: adapter.appKey || null,
|
|
69
|
-
available: adapter.type === 'schedule'
|
|
69
|
+
available: adapter.type === 'schedule' || adapter.type === 'manual'
|
|
70
70
|
? true
|
|
71
71
|
: this._hasConnectedApp(userId, agentId, adapter.providerKey, adapter.appKey),
|
|
72
72
|
}));
|
|
@@ -243,7 +243,11 @@ class WidgetService {
|
|
|
243
243
|
WHERE id = ? AND user_id = ?`
|
|
244
244
|
).get(widgetId, userId);
|
|
245
245
|
if (!row) return null;
|
|
246
|
-
return this._serializeWidget(
|
|
246
|
+
return this._serializeWidget(
|
|
247
|
+
row,
|
|
248
|
+
this._loadLatestSnapshotMap([widgetId]).get(widgetId) || null,
|
|
249
|
+
this._loadWidgetTasksMap([widgetId], userId).get(widgetId) || []
|
|
250
|
+
);
|
|
247
251
|
}
|
|
248
252
|
|
|
249
253
|
listWidgets(userId, { agentId = null } = {}) {
|
|
@@ -262,7 +266,8 @@ class WidgetService {
|
|
|
262
266
|
ORDER BY updated_at DESC, created_at DESC`
|
|
263
267
|
).all(userId);
|
|
264
268
|
const snapshotMap = this._loadLatestSnapshotMap(rows.map((row) => row.id));
|
|
265
|
-
|
|
269
|
+
const tasksMap = this._loadWidgetTasksMap(rows.map((row) => row.id), userId);
|
|
270
|
+
return rows.map((row) => this._serializeWidget(row, snapshotMap.get(row.id) || null, tasksMap.get(row.id) || []));
|
|
266
271
|
}
|
|
267
272
|
|
|
268
273
|
listLatestSnapshots(userId, { agentId = null } = {}) {
|
|
@@ -343,7 +348,7 @@ class WidgetService {
|
|
|
343
348
|
throw new Error('Widget not found.');
|
|
344
349
|
}
|
|
345
350
|
|
|
346
|
-
const current = this._serializeWidget(existingRow, null);
|
|
351
|
+
const current = this._serializeWidget(existingRow, null, []);
|
|
347
352
|
const normalized = normalizeWidgetInput({
|
|
348
353
|
name: input.name ?? current.name,
|
|
349
354
|
template: input.template ?? current.template,
|
|
@@ -581,7 +586,46 @@ class WidgetService {
|
|
|
581
586
|
return map;
|
|
582
587
|
}
|
|
583
588
|
|
|
584
|
-
|
|
589
|
+
_loadWidgetTasksMap(widgetIds, userId) {
|
|
590
|
+
const ids = Array.from(new Set(widgetIds.filter(Boolean)));
|
|
591
|
+
const map = new Map();
|
|
592
|
+
if (!ids.length) return map;
|
|
593
|
+
|
|
594
|
+
const placeholders = ids.map(() => '?').join(', ');
|
|
595
|
+
const params = [userId, ...ids];
|
|
596
|
+
|
|
597
|
+
// We filter tasks where task_type is NOT 'widget_refresh'
|
|
598
|
+
// and where the task_config contains the widgetId.
|
|
599
|
+
const rows = db.prepare(
|
|
600
|
+
`SELECT id, name, trigger_type, enabled, task_config
|
|
601
|
+
FROM scheduled_tasks
|
|
602
|
+
WHERE user_id = ?
|
|
603
|
+
AND task_type != 'widget_refresh'
|
|
604
|
+
AND json_extract(task_config, '$.widgetId') IN (${placeholders})
|
|
605
|
+
ORDER BY created_at ASC`
|
|
606
|
+
).all(...params);
|
|
607
|
+
|
|
608
|
+
for (const row of rows) {
|
|
609
|
+
const config = parseJsonObject(row.task_config, {});
|
|
610
|
+
const widgetId = config.widgetId;
|
|
611
|
+
if (!widgetId) continue;
|
|
612
|
+
|
|
613
|
+
const task = {
|
|
614
|
+
id: row.id,
|
|
615
|
+
name: row.name,
|
|
616
|
+
triggerType: row.trigger_type,
|
|
617
|
+
enabled: row.enabled !== 0 && row.enabled !== false,
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
if (!map.has(widgetId)) {
|
|
621
|
+
map.set(widgetId, []);
|
|
622
|
+
}
|
|
623
|
+
map.get(widgetId).push(task);
|
|
624
|
+
}
|
|
625
|
+
return map;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
_serializeWidget(row, latestSnapshot, tasks = []) {
|
|
585
629
|
const definition = parseJsonObject(row.definition_json, {});
|
|
586
630
|
return {
|
|
587
631
|
id: row.id,
|
|
@@ -600,6 +644,7 @@ class WidgetService {
|
|
|
600
644
|
updatedAt: row.updated_at || null,
|
|
601
645
|
nextRefresh: row.refresh_cron ? findNextRun(row.refresh_cron)?.toISOString() || null : null,
|
|
602
646
|
latestSnapshot,
|
|
647
|
+
tasks,
|
|
603
648
|
};
|
|
604
649
|
}
|
|
605
650
|
}
|