neoagent 2.2.1-beta.6 → 2.2.1-beta.7
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/docs/automation.md +2 -2
- package/docs/capabilities.md +7 -10
- package/docs/hardware.md +4 -7
- package/docs/index.md +6 -7
- package/docs/integrations.md +1 -1
- package/docs/operations.md +1 -1
- package/docs/why-neoagent.md +2 -2
- package/package.json +1 -1
- package/server/db/database.js +76 -61
- package/server/http/routes.js +1 -2
- package/server/public/assets/AssetManifest.json +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +65118 -64805
- package/server/routes/{scheduler.js → tasks.js} +31 -29
- package/server/routes/widgets.js +7 -7
- package/server/services/ai/capabilityHealth.js +4 -4
- package/server/services/ai/engine.js +9 -9
- package/server/services/ai/systemPrompt.js +7 -7
- package/server/services/ai/taskAnalysis.js +3 -3
- package/server/services/ai/toolResult.js +6 -8
- package/server/services/ai/tools.js +62 -95
- package/server/services/commands/router.js +14 -6
- package/server/services/integrations/whatsapp/provider.js +23 -1
- package/server/services/manager.js +14 -14
- package/server/services/memory/manager.js +7 -7
- package/server/services/memory/policy.js +1 -1
- package/server/services/messaging/formatting_guides.js +0 -4
- package/server/services/messaging/manager.js +0 -2
- package/server/services/tasks/adapters/gmail_message_received.js +36 -0
- package/server/services/tasks/adapters/index.js +10 -0
- package/server/services/tasks/adapters/outlook_email_received.js +38 -0
- package/server/services/tasks/adapters/schedule.js +57 -0
- package/server/services/tasks/adapters/slack_message_received.js +39 -0
- package/server/services/tasks/adapters/teams_message_received.js +39 -0
- package/server/services/tasks/adapters/whatsapp_personal_message_received.js +42 -0
- package/server/services/tasks/integration_runtime.js +260 -0
- package/server/services/tasks/runtime.js +539 -0
- package/server/services/{scheduler/cron_utils.js → tasks/schedule_utils.js} +2 -0
- package/server/services/tasks/security.js +60 -0
- package/server/services/tasks/task_repository.js +162 -0
- package/server/services/tasks/trigger_registry.js +29 -0
- package/server/services/tasks/utils.js +45 -0
- package/server/services/websocket.js +1 -1
- package/server/services/widgets/service.js +37 -25
- package/server/routes/wearable_device.js +0 -147
- package/server/services/messaging/waveshare_wearable.js +0 -40
- package/server/services/scheduler/cron.js +0 -580
- package/server/services/wearables/device_auth.js +0 -228
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const db = require('../../db/database');
|
|
4
|
+
|
|
5
|
+
class TaskRepository {
|
|
6
|
+
createTask(userId, normalizedTask) {
|
|
7
|
+
const result = db.prepare(
|
|
8
|
+
`INSERT INTO scheduled_tasks (
|
|
9
|
+
user_id, agent_id, name, trigger_type, trigger_config, cron_expression, run_at, one_time,
|
|
10
|
+
execution_mode, task_type, task_config, enabled
|
|
11
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
12
|
+
).run(
|
|
13
|
+
userId,
|
|
14
|
+
normalizedTask.agentId,
|
|
15
|
+
normalizedTask.name,
|
|
16
|
+
normalizedTask.triggerType,
|
|
17
|
+
JSON.stringify(normalizedTask.triggerConfig),
|
|
18
|
+
normalizedTask.legacyCronExpression,
|
|
19
|
+
normalizedTask.legacyRunAt,
|
|
20
|
+
normalizedTask.legacyOneTime ? 1 : 0,
|
|
21
|
+
normalizedTask.executionMode,
|
|
22
|
+
normalizedTask.taskType,
|
|
23
|
+
JSON.stringify(normalizedTask.taskConfig),
|
|
24
|
+
normalizedTask.enabled ? 1 : 0,
|
|
25
|
+
);
|
|
26
|
+
return result.lastInsertRowid;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
updateTask(taskId, userId, normalizedTask) {
|
|
30
|
+
db.prepare(
|
|
31
|
+
`UPDATE scheduled_tasks
|
|
32
|
+
SET agent_id = ?, name = ?, trigger_type = ?, trigger_config = ?, cron_expression = ?, run_at = ?,
|
|
33
|
+
one_time = ?, execution_mode = ?, task_type = ?, task_config = ?, enabled = ?
|
|
34
|
+
WHERE id = ? AND user_id = ?`
|
|
35
|
+
).run(
|
|
36
|
+
normalizedTask.agentId,
|
|
37
|
+
normalizedTask.name,
|
|
38
|
+
normalizedTask.triggerType,
|
|
39
|
+
JSON.stringify(normalizedTask.triggerConfig),
|
|
40
|
+
normalizedTask.legacyCronExpression,
|
|
41
|
+
normalizedTask.legacyRunAt,
|
|
42
|
+
normalizedTask.legacyOneTime ? 1 : 0,
|
|
43
|
+
normalizedTask.executionMode,
|
|
44
|
+
normalizedTask.taskType,
|
|
45
|
+
JSON.stringify(normalizedTask.taskConfig),
|
|
46
|
+
normalizedTask.enabled ? 1 : 0,
|
|
47
|
+
taskId,
|
|
48
|
+
userId,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
deleteTask(taskId, userId) {
|
|
53
|
+
db.prepare('DELETE FROM scheduled_tasks WHERE id = ? AND user_id = ?').run(taskId, userId);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
deleteById(taskId, userId) {
|
|
57
|
+
db.prepare('DELETE FROM scheduled_tasks WHERE id = ? AND user_id = ?').run(taskId, userId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getTaskById(taskId, userId) {
|
|
61
|
+
return db.prepare('SELECT * FROM scheduled_tasks WHERE id = ? AND user_id = ?').get(taskId, userId);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
listTasksForAgent(userId, agentId, includeLegacyMainTasks) {
|
|
65
|
+
return includeLegacyMainTasks
|
|
66
|
+
? db.prepare(
|
|
67
|
+
`SELECT * FROM scheduled_tasks
|
|
68
|
+
WHERE user_id = ? AND (agent_id = ? OR agent_id IS NULL)
|
|
69
|
+
ORDER BY created_at DESC`
|
|
70
|
+
).all(userId, agentId)
|
|
71
|
+
: db.prepare(
|
|
72
|
+
`SELECT * FROM scheduled_tasks
|
|
73
|
+
WHERE user_id = ? AND agent_id = ?
|
|
74
|
+
ORDER BY created_at DESC`
|
|
75
|
+
).all(userId, agentId);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
listEnabledTasks() {
|
|
79
|
+
return db.prepare('SELECT * FROM scheduled_tasks WHERE enabled = 1').all();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
listDueOneTimeTasks() {
|
|
83
|
+
return db.prepare(
|
|
84
|
+
`SELECT * FROM scheduled_tasks
|
|
85
|
+
WHERE trigger_type = 'schedule'
|
|
86
|
+
AND one_time = 1
|
|
87
|
+
AND enabled = 1
|
|
88
|
+
AND run_at IS NOT NULL
|
|
89
|
+
AND run_at <= datetime('now')`
|
|
90
|
+
).all();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
listEnabledByTriggerTypes(triggerTypes) {
|
|
94
|
+
if (!Array.isArray(triggerTypes) || triggerTypes.length === 0) {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
const placeholders = triggerTypes.map(() => '?').join(', ');
|
|
98
|
+
return db.prepare(
|
|
99
|
+
`SELECT * FROM scheduled_tasks
|
|
100
|
+
WHERE enabled = 1
|
|
101
|
+
AND trigger_type IN (${placeholders})`
|
|
102
|
+
).all(...triggerTypes);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
listEnabledWhatsappEventTasks(userId, agentId) {
|
|
106
|
+
return db.prepare(
|
|
107
|
+
`SELECT * FROM scheduled_tasks
|
|
108
|
+
WHERE enabled = 1 AND user_id = ? AND agent_id = ? AND trigger_type = 'whatsapp_personal_message_received'`
|
|
109
|
+
).all(userId, agentId);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
markTaskTriggered(taskId, userId, fingerprint) {
|
|
113
|
+
db.prepare(
|
|
114
|
+
`UPDATE scheduled_tasks
|
|
115
|
+
SET last_triggered_at = datetime('now'), last_trigger_fingerprint = ?
|
|
116
|
+
WHERE id = ? AND user_id = ?`
|
|
117
|
+
).run(fingerprint, taskId, userId);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
markTaskTriggerCheckpoint(taskId, fingerprint) {
|
|
121
|
+
db.prepare(
|
|
122
|
+
`UPDATE scheduled_tasks
|
|
123
|
+
SET last_triggered_at = datetime('now'), last_trigger_fingerprint = ?
|
|
124
|
+
WHERE id = ?`
|
|
125
|
+
).run(fingerprint, taskId);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
markTaskRun(taskId, userId) {
|
|
129
|
+
db.prepare('UPDATE scheduled_tasks SET last_run = datetime(\'now\') WHERE id = ? AND user_id = ?').run(taskId, userId);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
updateTaskConfig(taskId, userId, taskConfig) {
|
|
133
|
+
db.prepare('UPDATE scheduled_tasks SET task_config = ? WHERE id = ? AND user_id = ?')
|
|
134
|
+
.run(JSON.stringify(taskConfig), taskId, userId);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
getAgentSetting(userId, agentId, key) {
|
|
138
|
+
return db.prepare('SELECT value FROM agent_settings WHERE user_id = ? AND agent_id = ? AND key = ?')
|
|
139
|
+
.get(userId, agentId, key);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getUserSetting(userId, key) {
|
|
143
|
+
return db.prepare('SELECT value FROM user_settings WHERE user_id = ? AND key = ?')
|
|
144
|
+
.get(userId, key);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
getTaskConversation(userId, agentId, platform, platformChatId) {
|
|
148
|
+
return db.prepare(
|
|
149
|
+
'SELECT id FROM conversations WHERE user_id = ? AND agent_id = ? AND platform = ? AND platform_chat_id = ?'
|
|
150
|
+
).get(userId, agentId, platform, platformChatId);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
createTaskConversation({ id, userId, agentId, platform, platformChatId, title }) {
|
|
154
|
+
db.prepare(
|
|
155
|
+
'INSERT INTO conversations (id, user_id, agent_id, platform, platform_chat_id, title) VALUES (?, ?, ?, ?, ?, ?)'
|
|
156
|
+
).run(id, userId, agentId, platform, platformChatId, title);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
TaskRepository,
|
|
162
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
class TriggerRegistry {
|
|
4
|
+
constructor(adapters = []) {
|
|
5
|
+
this.adapters = new Map();
|
|
6
|
+
for (const adapter of adapters) {
|
|
7
|
+
this.register(adapter);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
register(adapter) {
|
|
12
|
+
if (!adapter?.type) {
|
|
13
|
+
throw new Error('Trigger adapters must define a type.');
|
|
14
|
+
}
|
|
15
|
+
this.adapters.set(adapter.type, adapter);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get(type) {
|
|
19
|
+
return this.adapters.get(String(type || '').trim()) || null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
list() {
|
|
23
|
+
return Array.from(this.adapters.values());
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
TriggerRegistry,
|
|
29
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function normalizeJsonObject(value, fallback = {}) {
|
|
4
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
5
|
+
return { ...fallback, ...value };
|
|
6
|
+
}
|
|
7
|
+
try {
|
|
8
|
+
const parsed = JSON.parse(String(value || '{}'));
|
|
9
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
10
|
+
? { ...fallback, ...parsed }
|
|
11
|
+
: { ...fallback };
|
|
12
|
+
} catch {
|
|
13
|
+
return { ...fallback };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function stringifyJson(value) {
|
|
18
|
+
const normalized = normalizeJsonObject(value);
|
|
19
|
+
try {
|
|
20
|
+
return JSON.stringify(normalized);
|
|
21
|
+
} catch {
|
|
22
|
+
try {
|
|
23
|
+
const seen = new WeakSet();
|
|
24
|
+
return JSON.stringify(normalized, (_key, currentValue) => {
|
|
25
|
+
if (typeof currentValue === 'bigint') {
|
|
26
|
+
return currentValue.toString();
|
|
27
|
+
}
|
|
28
|
+
if (currentValue && typeof currentValue === 'object') {
|
|
29
|
+
if (seen.has(currentValue)) {
|
|
30
|
+
return '[Circular]';
|
|
31
|
+
}
|
|
32
|
+
seen.add(currentValue);
|
|
33
|
+
}
|
|
34
|
+
return currentValue;
|
|
35
|
+
});
|
|
36
|
+
} catch {
|
|
37
|
+
return JSON.stringify(String(value));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = {
|
|
43
|
+
normalizeJsonObject,
|
|
44
|
+
stringifyJson,
|
|
45
|
+
};
|
|
@@ -160,7 +160,7 @@ function recordRateLimitHit(observer, userId, socketId, eventName, retryAfterMs)
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
function setupWebSocket(io, services) {
|
|
163
|
-
const { agentEngine, messagingManager, mcpClient,
|
|
163
|
+
const { agentEngine, messagingManager, mcpClient, taskRuntime, memoryManager, voiceRuntimeManager } = services;
|
|
164
164
|
const rateLimitObserver = createRateLimitObserver();
|
|
165
165
|
const integrationManager =
|
|
166
166
|
services.integrationManager || services.app?.locals?.integrationManager || null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const crypto = require('crypto');
|
|
2
2
|
const db = require('../../db/database');
|
|
3
3
|
const { resolveAgentId } = require('../agents/manager');
|
|
4
|
-
const { getMinimumIntervalMinutes } = require('../
|
|
4
|
+
const { findNextRun, getMinimumIntervalMinutes } = require('../tasks/schedule_utils');
|
|
5
5
|
|
|
6
6
|
const MIN_WIDGET_REFRESH_MINUTES = 60;
|
|
7
7
|
|
|
@@ -228,8 +228,8 @@ class WidgetService {
|
|
|
228
228
|
this.app = app;
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
get
|
|
232
|
-
return this.app?.locals?.
|
|
231
|
+
get taskRuntime() {
|
|
232
|
+
return this.app?.locals?.taskRuntime || null;
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
get agentEngine() {
|
|
@@ -271,11 +271,11 @@ class WidgetService {
|
|
|
271
271
|
.filter(Boolean);
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
createWidget(userId, input = {}) {
|
|
274
|
+
async createWidget(userId, input = {}) {
|
|
275
275
|
const normalized = normalizeWidgetInput(input, userId);
|
|
276
|
-
const
|
|
277
|
-
if (!
|
|
278
|
-
throw new Error('
|
|
276
|
+
const taskRuntime = this.taskRuntime;
|
|
277
|
+
if (!taskRuntime) {
|
|
278
|
+
throw new Error('Task runtime not available.');
|
|
279
279
|
}
|
|
280
280
|
const widgetId = crypto.randomUUID();
|
|
281
281
|
|
|
@@ -302,9 +302,13 @@ class WidgetService {
|
|
|
302
302
|
|
|
303
303
|
let task;
|
|
304
304
|
try {
|
|
305
|
-
task =
|
|
305
|
+
task = await taskRuntime.createTask(userId, {
|
|
306
306
|
name: buildWidgetRefreshTaskName(normalized.name),
|
|
307
|
-
|
|
307
|
+
triggerType: 'schedule',
|
|
308
|
+
triggerConfig: {
|
|
309
|
+
mode: 'recurring',
|
|
310
|
+
cronExpression: normalized.refreshCron,
|
|
311
|
+
},
|
|
308
312
|
enabled: normalized.enabled,
|
|
309
313
|
agentId: normalized.agentId,
|
|
310
314
|
taskType: 'widget_refresh',
|
|
@@ -323,7 +327,7 @@ class WidgetService {
|
|
|
323
327
|
).run(task.id, widgetId, userId);
|
|
324
328
|
} catch (error) {
|
|
325
329
|
try {
|
|
326
|
-
|
|
330
|
+
taskRuntime.deleteTask(task.id, userId, { allowManaged: true });
|
|
327
331
|
} catch {
|
|
328
332
|
// Ignore cleanup failures and rethrow the original DB error.
|
|
329
333
|
}
|
|
@@ -333,7 +337,7 @@ class WidgetService {
|
|
|
333
337
|
return this.getWidget(userId, widgetId);
|
|
334
338
|
}
|
|
335
339
|
|
|
336
|
-
updateWidget(userId, widgetId, input = {}) {
|
|
340
|
+
async updateWidget(userId, widgetId, input = {}) {
|
|
337
341
|
const existingRow = db.prepare('SELECT * FROM ai_widgets WHERE id = ? AND user_id = ?').get(widgetId, userId);
|
|
338
342
|
if (!existingRow) {
|
|
339
343
|
throw new Error('Widget not found.');
|
|
@@ -353,9 +357,9 @@ class WidgetService {
|
|
|
353
357
|
description: input.description,
|
|
354
358
|
}, userId);
|
|
355
359
|
|
|
356
|
-
const
|
|
357
|
-
if (!
|
|
358
|
-
throw new Error('
|
|
360
|
+
const taskRuntime = this.taskRuntime;
|
|
361
|
+
if (!taskRuntime) {
|
|
362
|
+
throw new Error('Task runtime not available.');
|
|
359
363
|
}
|
|
360
364
|
|
|
361
365
|
db.prepare('BEGIN').run();
|
|
@@ -378,12 +382,16 @@ class WidgetService {
|
|
|
378
382
|
);
|
|
379
383
|
|
|
380
384
|
if (existingRow.scheduled_task_id) {
|
|
381
|
-
|
|
385
|
+
await taskRuntime.updateTask(
|
|
382
386
|
existingRow.scheduled_task_id,
|
|
383
387
|
userId,
|
|
384
388
|
{
|
|
385
389
|
name: buildWidgetRefreshTaskName(normalized.name),
|
|
386
|
-
|
|
390
|
+
triggerType: 'schedule',
|
|
391
|
+
triggerConfig: {
|
|
392
|
+
mode: 'recurring',
|
|
393
|
+
cronExpression: normalized.refreshCron,
|
|
394
|
+
},
|
|
387
395
|
enabled: normalized.enabled,
|
|
388
396
|
agentId: normalized.agentId,
|
|
389
397
|
taskConfig: { widgetId },
|
|
@@ -391,9 +399,13 @@ class WidgetService {
|
|
|
391
399
|
{ allowManaged: true },
|
|
392
400
|
);
|
|
393
401
|
} else {
|
|
394
|
-
const task =
|
|
402
|
+
const task = await taskRuntime.createTask(userId, {
|
|
395
403
|
name: buildWidgetRefreshTaskName(normalized.name),
|
|
396
|
-
|
|
404
|
+
triggerType: 'schedule',
|
|
405
|
+
triggerConfig: {
|
|
406
|
+
mode: 'recurring',
|
|
407
|
+
cronExpression: normalized.refreshCron,
|
|
408
|
+
},
|
|
397
409
|
enabled: normalized.enabled,
|
|
398
410
|
agentId: normalized.agentId,
|
|
399
411
|
taskType: 'widget_refresh',
|
|
@@ -411,7 +423,7 @@ class WidgetService {
|
|
|
411
423
|
try {
|
|
412
424
|
db.prepare('ROLLBACK').run();
|
|
413
425
|
} catch {
|
|
414
|
-
// Ignore rollback failures and rethrow the original
|
|
426
|
+
// Ignore rollback failures and rethrow the original task runtime/DB error.
|
|
415
427
|
}
|
|
416
428
|
throw error;
|
|
417
429
|
}
|
|
@@ -425,10 +437,10 @@ class WidgetService {
|
|
|
425
437
|
throw new Error('Widget not found.');
|
|
426
438
|
}
|
|
427
439
|
|
|
428
|
-
const
|
|
440
|
+
const taskRuntime = this.taskRuntime;
|
|
429
441
|
const tx = db.transaction(() => {
|
|
430
|
-
if (existingRow.scheduled_task_id &&
|
|
431
|
-
|
|
442
|
+
if (existingRow.scheduled_task_id && taskRuntime) {
|
|
443
|
+
taskRuntime.deleteTask(existingRow.scheduled_task_id, userId, { allowManaged: true });
|
|
432
444
|
}
|
|
433
445
|
db.prepare('DELETE FROM ai_widget_snapshots WHERE widget_id = ?').run(widgetId);
|
|
434
446
|
db.prepare('DELETE FROM ai_widgets WHERE id = ? AND user_id = ?').run(widgetId, userId);
|
|
@@ -484,8 +496,8 @@ class WidgetService {
|
|
|
484
496
|
try {
|
|
485
497
|
const prompt = this._buildRefreshPrompt(widget);
|
|
486
498
|
const result = await engine.run(userId, prompt, {
|
|
487
|
-
triggerType: '
|
|
488
|
-
triggerSource: '
|
|
499
|
+
triggerType: 'schedule',
|
|
500
|
+
triggerSource: 'tasks',
|
|
489
501
|
agentId: widget.agentId,
|
|
490
502
|
app: this.app,
|
|
491
503
|
taskId: options.taskId || widget.scheduledTaskId || null,
|
|
@@ -586,7 +598,7 @@ class WidgetService {
|
|
|
586
598
|
lastError: row.last_error || null,
|
|
587
599
|
createdAt: row.created_at || null,
|
|
588
600
|
updatedAt: row.updated_at || null,
|
|
589
|
-
nextRefresh: row.refresh_cron ?
|
|
601
|
+
nextRefresh: row.refresh_cron ? findNextRun(row.refresh_cron)?.toISOString() || null : null,
|
|
590
602
|
latestSnapshot,
|
|
591
603
|
};
|
|
592
604
|
}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const express = require('express');
|
|
4
|
-
const { sanitizeError } = require('../utils/security');
|
|
5
|
-
const { getAgentIdFromRequest, resolveAgentId } = require('../services/agents/manager');
|
|
6
|
-
const { wearableDeviceAuth } = require('../services/wearables/device_auth');
|
|
7
|
-
const { createVoiceMessage } = require('../services/voice/message');
|
|
8
|
-
|
|
9
|
-
const router = express.Router();
|
|
10
|
-
|
|
11
|
-
router.post('/pairing/code', (req, res) => {
|
|
12
|
-
try {
|
|
13
|
-
const sessionUserId = req.session?.userId;
|
|
14
|
-
if (!sessionUserId) {
|
|
15
|
-
return res.status(401).json({ error: 'Authentication required' });
|
|
16
|
-
}
|
|
17
|
-
const agentId = resolveAgentId(sessionUserId, getAgentIdFromRequest(req));
|
|
18
|
-
const pairing = wearableDeviceAuth.createPairingCode(req.session.userId, {
|
|
19
|
-
agentId,
|
|
20
|
-
ttlMinutes: req.body?.ttlMinutes,
|
|
21
|
-
source: 'messaging_tab',
|
|
22
|
-
deviceHint: req.body?.deviceHint,
|
|
23
|
-
});
|
|
24
|
-
res.status(201).json(pairing);
|
|
25
|
-
} catch (err) {
|
|
26
|
-
res.status(500).json({ error: sanitizeError(err) });
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
function extractBearerToken(req) {
|
|
31
|
-
const auth = String(req.get('authorization') || '');
|
|
32
|
-
if (!auth.toLowerCase().startsWith('bearer ')) return null;
|
|
33
|
-
return auth.slice(7).trim();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function requireWearableToken(req, res, next) {
|
|
37
|
-
const token = extractBearerToken(req);
|
|
38
|
-
if (!token) return res.status(401).json({ error: 'Missing bearer token' });
|
|
39
|
-
const tokenRow = wearableDeviceAuth.validateBearerToken(token);
|
|
40
|
-
if (!tokenRow) return res.status(401).json({ error: 'Invalid wearable token' });
|
|
41
|
-
wearableDeviceAuth.touchToken(tokenRow.id);
|
|
42
|
-
req.wearableToken = tokenRow;
|
|
43
|
-
next();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
router.post('/pair/claim', (req, res) => {
|
|
47
|
-
try {
|
|
48
|
-
const code = String(req.body?.code || '').trim();
|
|
49
|
-
if (!code) return res.status(400).json({ error: 'code is required' });
|
|
50
|
-
|
|
51
|
-
const claimed = wearableDeviceAuth.claimPairingCode(code, {
|
|
52
|
-
deviceId: req.body?.deviceId,
|
|
53
|
-
deviceName: req.body?.deviceName,
|
|
54
|
-
macAddress: req.body?.macAddress,
|
|
55
|
-
protocol: req.body?.protocol,
|
|
56
|
-
firmwareVersion: req.body?.firmwareVersion,
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
res.status(201).json({
|
|
60
|
-
token: claimed.token,
|
|
61
|
-
tokenId: claimed.tokenId,
|
|
62
|
-
agentId: claimed.agentId,
|
|
63
|
-
deviceId: claimed.deviceId,
|
|
64
|
-
deviceName: claimed.deviceName,
|
|
65
|
-
protocol: claimed.protocol,
|
|
66
|
-
});
|
|
67
|
-
} catch (err) {
|
|
68
|
-
res.status(err.status || 500).json({ error: sanitizeError(err) });
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
router.post('/utterance', requireWearableToken, async (req, res) => {
|
|
73
|
-
try {
|
|
74
|
-
const token = req.wearableToken;
|
|
75
|
-
const text = String(req.body?.text || '').trim();
|
|
76
|
-
if (!text) return res.status(400).json({ error: 'text is required' });
|
|
77
|
-
|
|
78
|
-
const messagingManager = req.app.locals.messagingManager;
|
|
79
|
-
if (!messagingManager) {
|
|
80
|
-
return res.status(503).json({ error: 'Agent services unavailable' });
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const chatId = token.device_id || token.id;
|
|
84
|
-
const message = createVoiceMessage({
|
|
85
|
-
platform: 'waveshare_wearable',
|
|
86
|
-
agentId: token.agent_id || null,
|
|
87
|
-
chatId,
|
|
88
|
-
sender: chatId,
|
|
89
|
-
senderName: token.device_name || 'NeoOS Wearable',
|
|
90
|
-
content: text,
|
|
91
|
-
mediaType: 'audio',
|
|
92
|
-
metadata: {
|
|
93
|
-
source: 'wearable_device_token',
|
|
94
|
-
},
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
await messagingManager.ingestMessage(
|
|
98
|
-
token.user_id,
|
|
99
|
-
'waveshare_wearable',
|
|
100
|
-
message,
|
|
101
|
-
{ agentId: token.agent_id || null },
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
res.status(202).json({ success: true, accepted: true });
|
|
105
|
-
} catch (err) {
|
|
106
|
-
res.status(500).json({ error: sanitizeError(err) });
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
router.get('/me', requireWearableToken, (req, res) => {
|
|
111
|
-
const token = req.wearableToken;
|
|
112
|
-
res.json({
|
|
113
|
-
deviceId: token.device_id,
|
|
114
|
-
name: token.device_name,
|
|
115
|
-
macAddress: token.mac_address,
|
|
116
|
-
protocol: token.protocol,
|
|
117
|
-
firmwareVersion: token.firmware_version,
|
|
118
|
-
lastSeenAt: token.last_seen_at,
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
router.get('/responses/next', requireWearableToken, (req, res) => {
|
|
123
|
-
try {
|
|
124
|
-
const token = req.wearableToken;
|
|
125
|
-
const limit = Number(req.query.limit || 5);
|
|
126
|
-
const responses = wearableDeviceAuth.getPendingResponses(token, limit);
|
|
127
|
-
res.json({ responses });
|
|
128
|
-
} catch (err) {
|
|
129
|
-
res.status(500).json({ error: sanitizeError(err) });
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
router.post('/responses/ack', requireWearableToken, (req, res) => {
|
|
134
|
-
try {
|
|
135
|
-
const token = req.wearableToken;
|
|
136
|
-
const lastMessageId = Number(req.body?.lastMessageId || 0);
|
|
137
|
-
if (!Number.isFinite(lastMessageId) || lastMessageId <= 0) {
|
|
138
|
-
return res.status(400).json({ error: 'lastMessageId must be a positive number' });
|
|
139
|
-
}
|
|
140
|
-
wearableDeviceAuth.setLastCursor(token.id, lastMessageId);
|
|
141
|
-
res.json({ success: true, lastMessageId });
|
|
142
|
-
} catch (err) {
|
|
143
|
-
res.status(500).json({ error: sanitizeError(err) });
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
module.exports = router;
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
const db = require('../../db/database');
|
|
2
|
-
const { BasePlatform } = require('./base');
|
|
3
|
-
const { wearableDeviceAuth } = require('../wearables/device_auth');
|
|
4
|
-
|
|
5
|
-
class WaveshareWearablePlatform extends BasePlatform {
|
|
6
|
-
constructor(config = {}) {
|
|
7
|
-
super('waveshare_wearable', config);
|
|
8
|
-
this.supportsGroups = false;
|
|
9
|
-
this.supportsMedia = false;
|
|
10
|
-
this.supportsVoice = false;
|
|
11
|
-
this.status = 'disconnected';
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async connect() {
|
|
15
|
-
this.status = 'connected';
|
|
16
|
-
this.emit('connected');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async disconnect() {
|
|
20
|
-
this.status = 'disconnected';
|
|
21
|
-
this.emit('disconnected', { reason: 'manual' });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async sendMessage() {
|
|
25
|
-
this.emit('message_sent');
|
|
26
|
-
return { queued: true };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
getAuthInfo() {
|
|
30
|
-
return {
|
|
31
|
-
label: this.config?.deviceLabel || 'Wearable provisioning enabled',
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
listDevices(userId, options = {}) {
|
|
36
|
-
return wearableDeviceAuth.listDevicesForUser(userId, options);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
module.exports = { WaveshareWearablePlatform };
|