@naisys/hub 3.0.0-beta.6
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/bin/naisys-hub +2 -0
- package/dist/handlers/hubAccessKeyService.js +28 -0
- package/dist/handlers/hubAgentService.js +238 -0
- package/dist/handlers/hubAttachmentService.js +227 -0
- package/dist/handlers/hubConfigService.js +85 -0
- package/dist/handlers/hubCostService.js +279 -0
- package/dist/handlers/hubHeartbeatService.js +132 -0
- package/dist/handlers/hubHostService.js +35 -0
- package/dist/handlers/hubLogService.js +121 -0
- package/dist/handlers/hubMailService.js +397 -0
- package/dist/handlers/hubModelsService.js +106 -0
- package/dist/handlers/hubRunService.js +96 -0
- package/dist/handlers/hubSendMailService.js +126 -0
- package/dist/handlers/hubUserService.js +62 -0
- package/dist/naisysHub.js +129 -0
- package/dist/services/agentRegistrar.js +53 -0
- package/dist/services/certService.js +78 -0
- package/dist/services/hostRegistrar.js +82 -0
- package/dist/services/hubServerLog.js +47 -0
- package/dist/services/naisysConnection.js +45 -0
- package/dist/services/naisysServer.js +164 -0
- package/package.json +64 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { HubEvents, MailArchiveRequestSchema, MailListRequestSchema, MailMarkReadRequestSchema, MailPeekRequestSchema, MailSearchRequestSchema, MailSendRequestSchema, MailUnreadRequestSchema, } from "@naisys/hub-protocol";
|
|
2
|
+
const MAIL_AUTOSTART_CHECK_INTERVAL_MS = 10_000;
|
|
3
|
+
/** Handles mail events from NAISYS instances */
|
|
4
|
+
export function createHubMailService(naisysServer, { hubDb }, logService, heartbeatService, sendMailService, agentService, costService, configService) {
|
|
5
|
+
/** Check for inactive users with unread mail and trigger auto-start for each */
|
|
6
|
+
async function checkPendingAutoStarts() {
|
|
7
|
+
try {
|
|
8
|
+
const config = configService.getConfig();
|
|
9
|
+
if (!config.success || !config.config?.autoStartAgentsOnMessage)
|
|
10
|
+
return;
|
|
11
|
+
const activeUserIds = heartbeatService.getActiveUserIds();
|
|
12
|
+
// Find distinct users with unread mail (exclude 'from' type - senders pre-mark as read)
|
|
13
|
+
const unreadRecipients = await hubDb.mail_recipients.findMany({
|
|
14
|
+
where: {
|
|
15
|
+
read_at: null,
|
|
16
|
+
type: { not: "from" },
|
|
17
|
+
user: { enabled: true, archived: false },
|
|
18
|
+
},
|
|
19
|
+
select: {
|
|
20
|
+
user_id: true,
|
|
21
|
+
},
|
|
22
|
+
distinct: ["user_id"],
|
|
23
|
+
});
|
|
24
|
+
const inactiveUserIds = unreadRecipients
|
|
25
|
+
.map((recipient) => recipient.user_id)
|
|
26
|
+
.filter((userId) => !activeUserIds.has(userId));
|
|
27
|
+
if (inactiveUserIds.length === 0)
|
|
28
|
+
return;
|
|
29
|
+
await costService.checkSpendLimits(inactiveUserIds);
|
|
30
|
+
for (const userId of inactiveUserIds) {
|
|
31
|
+
if (heartbeatService.getActiveUserIds().has(userId))
|
|
32
|
+
continue;
|
|
33
|
+
if (costService.isUserSpendSuspended(userId))
|
|
34
|
+
continue;
|
|
35
|
+
void agentService.tryStartAgent(userId);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
logService.error(`[Hub:Mail] Auto-start check failed: ${error}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// When a NAISYS host connects, check for pending unread mail and auto-start agents
|
|
43
|
+
naisysServer.registerEvent(HubEvents.CLIENT_CONNECTED, (_hostId, connection) => {
|
|
44
|
+
if (connection.getHostType() === "naisys") {
|
|
45
|
+
void checkPendingAutoStarts();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
const pendingAutoStartInterval = setInterval(() => void checkPendingAutoStarts(), MAIL_AUTOSTART_CHECK_INTERVAL_MS);
|
|
49
|
+
// MAIL_SEND
|
|
50
|
+
naisysServer.registerEvent(HubEvents.MAIL_SEND, async (hostId, data, ack) => {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = MailSendRequestSchema.parse(data);
|
|
53
|
+
await sendMailService.sendMail({
|
|
54
|
+
fromUserId: parsed.fromUserId,
|
|
55
|
+
recipientUserIds: parsed.toUserIds,
|
|
56
|
+
subject: parsed.subject,
|
|
57
|
+
body: parsed.body,
|
|
58
|
+
kind: parsed.kind,
|
|
59
|
+
hostId,
|
|
60
|
+
attachmentIds: parsed.attachmentIds,
|
|
61
|
+
});
|
|
62
|
+
ack({ success: true });
|
|
63
|
+
void checkPendingAutoStarts();
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
logService.error(`[Hub:Mail] mail_send error from host ${hostId}: ${error}`);
|
|
67
|
+
ack({ success: false, error: String(error) });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
// MAIL_LIST
|
|
71
|
+
naisysServer.registerEvent(HubEvents.MAIL_LIST, async (hostId, data, ack) => {
|
|
72
|
+
try {
|
|
73
|
+
const parsed = MailListRequestSchema.parse(data);
|
|
74
|
+
// Build ownership condition based on filter
|
|
75
|
+
let ownershipCondition;
|
|
76
|
+
if (parsed.withUserIds?.length) {
|
|
77
|
+
// Messages between exactly this group of participants
|
|
78
|
+
const allUserIds = [...new Set([parsed.userId, ...parsed.withUserIds])];
|
|
79
|
+
const users = await hubDb.users.findMany({
|
|
80
|
+
where: { id: { in: allUserIds } },
|
|
81
|
+
select: { username: true },
|
|
82
|
+
});
|
|
83
|
+
const participants = users
|
|
84
|
+
.map((u) => u.username)
|
|
85
|
+
.sort()
|
|
86
|
+
.join(",");
|
|
87
|
+
ownershipCondition = { participants };
|
|
88
|
+
}
|
|
89
|
+
else if (parsed.filter === "received") {
|
|
90
|
+
ownershipCondition = {
|
|
91
|
+
recipients: { some: { user_id: parsed.userId } },
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
else if (parsed.filter === "sent") {
|
|
95
|
+
ownershipCondition = { from_user_id: parsed.userId };
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
ownershipCondition = {
|
|
99
|
+
OR: [
|
|
100
|
+
{ from_user_id: parsed.userId },
|
|
101
|
+
{ recipients: { some: { user_id: parsed.userId } } },
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const messages = await hubDb.mail_messages.findMany({
|
|
106
|
+
where: {
|
|
107
|
+
...ownershipCondition,
|
|
108
|
+
kind: parsed.kind,
|
|
109
|
+
NOT: {
|
|
110
|
+
recipients: {
|
|
111
|
+
some: {
|
|
112
|
+
user_id: parsed.userId,
|
|
113
|
+
archived_at: { not: null },
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
include: {
|
|
119
|
+
from_user: { select: { username: true, title: true } },
|
|
120
|
+
recipients: {
|
|
121
|
+
include: { user: { select: { username: true } } },
|
|
122
|
+
},
|
|
123
|
+
mail_attachments: {
|
|
124
|
+
include: {
|
|
125
|
+
attachment: {
|
|
126
|
+
select: { public_id: true, filename: true, file_size: true },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
orderBy: { created_at: "desc" },
|
|
132
|
+
skip: parsed.skip,
|
|
133
|
+
take: parsed.take ?? 20,
|
|
134
|
+
});
|
|
135
|
+
const messageData = messages.map((m) => {
|
|
136
|
+
const myRecipient = m.recipients.find((r) => r.user_id === parsed.userId && r.type !== "from");
|
|
137
|
+
const isUnread = m.from_user_id !== parsed.userId && !myRecipient?.read_at;
|
|
138
|
+
return {
|
|
139
|
+
id: m.id,
|
|
140
|
+
fromUsername: m.from_user.username,
|
|
141
|
+
fromTitle: m.from_user.title,
|
|
142
|
+
recipientUsernames: m.recipients
|
|
143
|
+
.filter((r) => r.type !== "from")
|
|
144
|
+
.map((r) => r.user.username),
|
|
145
|
+
subject: m.subject,
|
|
146
|
+
createdAt: m.created_at.toISOString(),
|
|
147
|
+
isUnread,
|
|
148
|
+
...(parsed.kind === "chat" ? { body: m.body } : {}),
|
|
149
|
+
attachments: m.mail_attachments.length
|
|
150
|
+
? m.mail_attachments.map((ma) => ({
|
|
151
|
+
id: ma.attachment.public_id,
|
|
152
|
+
filename: ma.attachment.filename,
|
|
153
|
+
fileSize: ma.attachment.file_size,
|
|
154
|
+
}))
|
|
155
|
+
: undefined,
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
ack({ success: true, messages: messageData });
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
logService.error(`[Hub:Mail] mail_list error from host ${hostId}: ${error}`);
|
|
162
|
+
ack({ success: false, error: String(error) });
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
// MAIL_PEEK
|
|
166
|
+
naisysServer.registerEvent(HubEvents.MAIL_PEEK, async (hostId, data, ack) => {
|
|
167
|
+
try {
|
|
168
|
+
const parsed = MailPeekRequestSchema.parse(data);
|
|
169
|
+
const message = await hubDb.mail_messages.findUnique({
|
|
170
|
+
where: { id: parsed.messageId },
|
|
171
|
+
include: {
|
|
172
|
+
from_user: { select: { username: true, title: true } },
|
|
173
|
+
recipients: {
|
|
174
|
+
include: { user: { select: { username: true } } },
|
|
175
|
+
},
|
|
176
|
+
mail_attachments: {
|
|
177
|
+
include: {
|
|
178
|
+
attachment: {
|
|
179
|
+
select: { public_id: true, filename: true, file_size: true },
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
if (!message) {
|
|
186
|
+
ack({
|
|
187
|
+
success: false,
|
|
188
|
+
error: `Message ${parsed.messageId} not found`,
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
ack({
|
|
193
|
+
success: true,
|
|
194
|
+
message: {
|
|
195
|
+
id: message.id,
|
|
196
|
+
subject: message.subject,
|
|
197
|
+
fromUsername: message.from_user.username,
|
|
198
|
+
fromTitle: message.from_user.title,
|
|
199
|
+
recipientUsernames: message.recipients
|
|
200
|
+
.filter((r) => r.type !== "from")
|
|
201
|
+
.map((r) => r.user.username),
|
|
202
|
+
createdAt: message.created_at.toISOString(),
|
|
203
|
+
body: message.body,
|
|
204
|
+
attachments: message.mail_attachments.length
|
|
205
|
+
? message.mail_attachments.map((ma) => ({
|
|
206
|
+
id: ma.attachment.public_id,
|
|
207
|
+
filename: ma.attachment.filename,
|
|
208
|
+
fileSize: ma.attachment.file_size,
|
|
209
|
+
}))
|
|
210
|
+
: undefined,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
logService.error(`[Hub:Mail] mail_peek error from host ${hostId}: ${error}`);
|
|
216
|
+
ack({ success: false, error: String(error) });
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
// MAIL_MARK_READ
|
|
220
|
+
naisysServer.registerEvent(HubEvents.MAIL_MARK_READ, async (hostId, data, ack) => {
|
|
221
|
+
try {
|
|
222
|
+
const parsed = MailMarkReadRequestSchema.parse(data);
|
|
223
|
+
const result = await hubDb.mail_recipients.updateMany({
|
|
224
|
+
where: {
|
|
225
|
+
message_id: { in: parsed.messageIds },
|
|
226
|
+
user_id: parsed.userId,
|
|
227
|
+
read_at: null,
|
|
228
|
+
},
|
|
229
|
+
data: { read_at: new Date() },
|
|
230
|
+
});
|
|
231
|
+
ack({ success: true });
|
|
232
|
+
// Push read receipts to supervisor connections
|
|
233
|
+
if (result.count > 0) {
|
|
234
|
+
const messages = await hubDb.mail_messages.findMany({
|
|
235
|
+
where: { id: { in: parsed.messageIds } },
|
|
236
|
+
select: { participants: true },
|
|
237
|
+
});
|
|
238
|
+
// participants is like the room id, we broadcast to all rooms the read message ids
|
|
239
|
+
// It's ok if the specific message id is not in the room, the client will ignore it
|
|
240
|
+
const participants = [
|
|
241
|
+
...new Set(messages.map((m) => m.participants)),
|
|
242
|
+
];
|
|
243
|
+
const payload = {
|
|
244
|
+
messageIds: parsed.messageIds,
|
|
245
|
+
userId: parsed.userId,
|
|
246
|
+
kind: parsed.kind,
|
|
247
|
+
participants,
|
|
248
|
+
};
|
|
249
|
+
naisysServer.broadcastToSupervisors(HubEvents.MAIL_READ_PUSH, payload);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
logService.error(`[Hub:Mail] mail_mark_read error from host ${hostId}: ${error}`);
|
|
254
|
+
ack({ success: false, error: String(error) });
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
// MAIL_ARCHIVE
|
|
258
|
+
naisysServer.registerEvent(HubEvents.MAIL_ARCHIVE, async (hostId, data, ack) => {
|
|
259
|
+
try {
|
|
260
|
+
const parsed = MailArchiveRequestSchema.parse(data);
|
|
261
|
+
const archivedIds = [];
|
|
262
|
+
for (const messageId of parsed.messageIds) {
|
|
263
|
+
const message = await hubDb.mail_messages.findUnique({
|
|
264
|
+
where: { id: messageId },
|
|
265
|
+
});
|
|
266
|
+
if (!message) {
|
|
267
|
+
ack({
|
|
268
|
+
success: false,
|
|
269
|
+
error: `Message ${messageId} not found`,
|
|
270
|
+
});
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
await hubDb.mail_recipients.updateMany({
|
|
274
|
+
where: { message_id: message.id, user_id: parsed.userId },
|
|
275
|
+
data: { archived_at: new Date() },
|
|
276
|
+
});
|
|
277
|
+
archivedIds.push(messageId);
|
|
278
|
+
}
|
|
279
|
+
ack({ success: true, archivedIds });
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
logService.error(`[Hub:Mail] mail_archive error from host ${hostId}: ${error}`);
|
|
283
|
+
ack({ success: false, error: String(error) });
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
// MAIL_SEARCH
|
|
287
|
+
naisysServer.registerEvent(HubEvents.MAIL_SEARCH, async (hostId, data, ack) => {
|
|
288
|
+
try {
|
|
289
|
+
const parsed = MailSearchRequestSchema.parse(data);
|
|
290
|
+
const searchCondition = parsed.subjectOnly
|
|
291
|
+
? { subject: { contains: parsed.terms } }
|
|
292
|
+
: {
|
|
293
|
+
OR: [
|
|
294
|
+
{ subject: { contains: parsed.terms } },
|
|
295
|
+
{ body: { contains: parsed.terms } },
|
|
296
|
+
],
|
|
297
|
+
};
|
|
298
|
+
const archiveCondition = parsed.includeArchived
|
|
299
|
+
? {}
|
|
300
|
+
: {
|
|
301
|
+
NOT: {
|
|
302
|
+
recipients: {
|
|
303
|
+
some: {
|
|
304
|
+
user_id: parsed.userId,
|
|
305
|
+
archived_at: { not: null },
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
const messages = await hubDb.mail_messages.findMany({
|
|
311
|
+
where: {
|
|
312
|
+
OR: [
|
|
313
|
+
{ from_user_id: parsed.userId },
|
|
314
|
+
{ recipients: { some: { user_id: parsed.userId } } },
|
|
315
|
+
],
|
|
316
|
+
...searchCondition,
|
|
317
|
+
...archiveCondition,
|
|
318
|
+
},
|
|
319
|
+
include: {
|
|
320
|
+
from_user: { select: { username: true } },
|
|
321
|
+
},
|
|
322
|
+
orderBy: { created_at: "desc" },
|
|
323
|
+
take: 50,
|
|
324
|
+
});
|
|
325
|
+
const messageData = messages.map((m) => ({
|
|
326
|
+
id: m.id,
|
|
327
|
+
subject: m.subject,
|
|
328
|
+
fromUsername: m.from_user.username,
|
|
329
|
+
createdAt: m.created_at.toISOString(),
|
|
330
|
+
}));
|
|
331
|
+
ack({ success: true, messages: messageData });
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
logService.error(`[Hub:Mail] mail_search error from host ${hostId}: ${error}`);
|
|
335
|
+
ack({ success: false, error: String(error) });
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
// MAIL_UNREAD
|
|
339
|
+
naisysServer.registerEvent(HubEvents.MAIL_UNREAD, async (hostId, data, ack) => {
|
|
340
|
+
try {
|
|
341
|
+
const parsed = MailUnreadRequestSchema.parse(data);
|
|
342
|
+
const messages = await hubDb.mail_messages.findMany({
|
|
343
|
+
where: {
|
|
344
|
+
kind: parsed.kind,
|
|
345
|
+
...(parsed.afterId ? { id: { gt: parsed.afterId } } : {}),
|
|
346
|
+
recipients: {
|
|
347
|
+
some: { user_id: parsed.userId, read_at: null },
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
include: {
|
|
351
|
+
from_user: { select: { username: true, title: true } },
|
|
352
|
+
recipients: {
|
|
353
|
+
include: { user: { select: { username: true } } },
|
|
354
|
+
},
|
|
355
|
+
mail_attachments: {
|
|
356
|
+
include: {
|
|
357
|
+
attachment: {
|
|
358
|
+
select: { public_id: true, filename: true, file_size: true },
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
orderBy: { id: "asc" },
|
|
364
|
+
});
|
|
365
|
+
ack({
|
|
366
|
+
success: true,
|
|
367
|
+
messages: messages.map((m) => ({
|
|
368
|
+
id: m.id,
|
|
369
|
+
subject: m.subject,
|
|
370
|
+
fromUsername: m.from_user.username,
|
|
371
|
+
fromTitle: m.from_user.title,
|
|
372
|
+
recipientUsernames: m.recipients
|
|
373
|
+
.filter((r) => r.type !== "from")
|
|
374
|
+
.map((r) => r.user.username),
|
|
375
|
+
createdAt: m.created_at.toISOString(),
|
|
376
|
+
body: m.body,
|
|
377
|
+
attachments: m.mail_attachments.length
|
|
378
|
+
? m.mail_attachments.map((ma) => ({
|
|
379
|
+
id: ma.attachment.public_id,
|
|
380
|
+
filename: ma.attachment.filename,
|
|
381
|
+
fileSize: ma.attachment.file_size,
|
|
382
|
+
}))
|
|
383
|
+
: undefined,
|
|
384
|
+
})),
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
logService.error(`[Hub:Mail] mail_unread error from host ${hostId}: ${error}`);
|
|
389
|
+
ack({ success: false, error: String(error) });
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
function cleanup() {
|
|
393
|
+
clearInterval(pendingAutoStartInterval);
|
|
394
|
+
}
|
|
395
|
+
return { cleanup };
|
|
396
|
+
}
|
|
397
|
+
//# sourceMappingURL=hubMailService.js.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { builtInImageModels, builtInLlmModels, dbFieldsToImageModel, dbFieldsToLlmModel, imageModelToDbFields, llmModelToDbFields, } from "@naisys/common";
|
|
2
|
+
import { loadCustomModels } from "@naisys/common-node";
|
|
3
|
+
import { HubEvents } from "@naisys/hub-protocol";
|
|
4
|
+
/** Hub handler that seeds models on startup, pushes them on connect, and broadcasts on change */
|
|
5
|
+
export async function createHubModelsService(naisysServer, { hubDb }, logService) {
|
|
6
|
+
// Seed models table from built-in + YAML custom models (one-time, skips if non-empty)
|
|
7
|
+
await seedModels(hubDb, logService);
|
|
8
|
+
async function buildModelsPayload() {
|
|
9
|
+
const rows = (await hubDb.models.findMany());
|
|
10
|
+
const llmModels = rows
|
|
11
|
+
.filter((r) => r.type === "llm")
|
|
12
|
+
.map((r) => dbFieldsToLlmModel(r));
|
|
13
|
+
const imageModels = rows
|
|
14
|
+
.filter((r) => r.type === "image")
|
|
15
|
+
.map((r) => dbFieldsToImageModel(r));
|
|
16
|
+
return { success: true, llmModels, imageModels };
|
|
17
|
+
}
|
|
18
|
+
async function broadcastModels() {
|
|
19
|
+
try {
|
|
20
|
+
const payload = await buildModelsPayload();
|
|
21
|
+
logService.log(`[Hub:Models] Broadcasting ${payload.llmModels?.length ?? 0} LLM + ${payload.imageModels?.length ?? 0} image models to all clients`);
|
|
22
|
+
naisysServer.broadcastToAll(HubEvents.MODELS_UPDATED, payload);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
logService.error(`[Hub:Models] Error broadcasting models: ${error}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Push models to newly connected clients
|
|
29
|
+
naisysServer.registerEvent(HubEvents.CLIENT_CONNECTED, async (hostId, connection) => {
|
|
30
|
+
try {
|
|
31
|
+
const payload = await buildModelsPayload();
|
|
32
|
+
logService.log(`[Hub:Models] Pushing ${payload.llmModels?.length ?? 0} LLM + ${payload.imageModels?.length ?? 0} image models to instance ${hostId}`);
|
|
33
|
+
connection.sendMessage(HubEvents.MODELS_UPDATED, payload);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
logService.error(`[Hub:Models] Error querying models for instance ${hostId}: ${error}`);
|
|
37
|
+
connection.sendMessage(HubEvents.MODELS_UPDATED, {
|
|
38
|
+
success: false,
|
|
39
|
+
error: String(error),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
// Broadcast models to all clients when supervisor saves/deletes a model
|
|
44
|
+
naisysServer.registerEvent(HubEvents.MODELS_CHANGED, async () => {
|
|
45
|
+
await broadcastModels();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** Seeds models table from built-in models + any YAML custom models.
|
|
49
|
+
* Built-in models are upserted on every startup unless the user has customized them.
|
|
50
|
+
* YAML custom models are only imported on first run (empty table). */
|
|
51
|
+
async function seedModels(hubDb, logService) {
|
|
52
|
+
const existingRows = (await hubDb.models.findMany());
|
|
53
|
+
const isFirstRun = existingRows.length === 0;
|
|
54
|
+
// Upsert built-in models that haven't been customized
|
|
55
|
+
const builtInFields = [
|
|
56
|
+
...builtInLlmModels.map((m) => llmModelToDbFields(m, true, false)),
|
|
57
|
+
...builtInImageModels.map((m) => imageModelToDbFields(m, true, false)),
|
|
58
|
+
];
|
|
59
|
+
for (const fields of builtInFields) {
|
|
60
|
+
const existing = existingRows.find((r) => r.key === fields.key);
|
|
61
|
+
if (existing?.is_custom) {
|
|
62
|
+
// User has customized this built-in model, don't overwrite
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (existing) {
|
|
66
|
+
await hubDb.models.update({ where: { key: fields.key }, data: fields });
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
await hubDb.models.create({ data: fields });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Import YAML custom models only on first run (migration from file-based storage)
|
|
73
|
+
if (isFirstRun) {
|
|
74
|
+
const custom = loadCustomModels(process.env.NAISYS_FOLDER || "");
|
|
75
|
+
const customRows = [];
|
|
76
|
+
for (const m of custom.llmModels ?? []) {
|
|
77
|
+
const isBuiltin = builtInLlmModels.some((b) => b.key === m.key);
|
|
78
|
+
const fields = llmModelToDbFields(m, isBuiltin, true);
|
|
79
|
+
if (isBuiltin) {
|
|
80
|
+
// Override the built-in row we just inserted
|
|
81
|
+
await hubDb.models.update({ where: { key: m.key }, data: fields });
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
customRows.push(fields);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
for (const m of custom.imageModels ?? []) {
|
|
88
|
+
const isBuiltin = builtInImageModels.some((b) => b.key === m.key);
|
|
89
|
+
const fields = imageModelToDbFields(m, isBuiltin, true);
|
|
90
|
+
if (isBuiltin) {
|
|
91
|
+
await hubDb.models.update({ where: { key: m.key }, data: fields });
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
customRows.push(fields);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (customRows.length > 0) {
|
|
98
|
+
await hubDb.models.createMany({ data: customRows });
|
|
99
|
+
}
|
|
100
|
+
logService.log(`[Hub:Models] First run: imported ${(custom.llmModels?.length ?? 0) + (custom.imageModels?.length ?? 0)} custom models from YAML`);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
logService.log(`[Hub:Models] Models already seeded`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=hubModelsService.js.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { HubEvents, SessionCreateRequestSchema, SessionIncrementRequestSchema, } from "@naisys/hub-protocol";
|
|
2
|
+
/** Handles session_create and session_increment requests from NAISYS instances */
|
|
3
|
+
export function createHubRunService(naisysServer, { hubDb }, logService) {
|
|
4
|
+
function pushSessionToSupervisors(session) {
|
|
5
|
+
naisysServer.broadcastToSupervisors(HubEvents.SESSION_PUSH, {
|
|
6
|
+
session: {
|
|
7
|
+
...session,
|
|
8
|
+
latestLogId: 0,
|
|
9
|
+
totalLines: 0,
|
|
10
|
+
totalCost: 0,
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
naisysServer.registerEvent(HubEvents.SESSION_CREATE, async (hostId, data, ack) => {
|
|
15
|
+
try {
|
|
16
|
+
const parsed = SessionCreateRequestSchema.parse(data);
|
|
17
|
+
// Get the last run_id across all sessions
|
|
18
|
+
const lastRun = await hubDb.run_session.findFirst({
|
|
19
|
+
select: { run_id: true },
|
|
20
|
+
orderBy: { run_id: "desc" },
|
|
21
|
+
});
|
|
22
|
+
const newRunId = lastRun ? lastRun.run_id + 1 : 1;
|
|
23
|
+
const newSessionId = 1;
|
|
24
|
+
const now = new Date().toISOString();
|
|
25
|
+
await hubDb.run_session.create({
|
|
26
|
+
data: {
|
|
27
|
+
user_id: parsed.userId,
|
|
28
|
+
run_id: newRunId,
|
|
29
|
+
session_id: newSessionId,
|
|
30
|
+
host_id: hostId,
|
|
31
|
+
model_name: parsed.modelName,
|
|
32
|
+
created_at: now,
|
|
33
|
+
last_active: now,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
ack({
|
|
37
|
+
success: true,
|
|
38
|
+
runId: newRunId,
|
|
39
|
+
sessionId: newSessionId,
|
|
40
|
+
});
|
|
41
|
+
pushSessionToSupervisors({
|
|
42
|
+
userId: parsed.userId,
|
|
43
|
+
runId: newRunId,
|
|
44
|
+
sessionId: newSessionId,
|
|
45
|
+
modelName: parsed.modelName,
|
|
46
|
+
createdAt: now,
|
|
47
|
+
lastActive: now,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
logService.error(`[Hub:Runs] session_create error for host ${hostId}: ${error}`);
|
|
52
|
+
ack({ success: false, error: String(error) });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
naisysServer.registerEvent(HubEvents.SESSION_INCREMENT, async (hostId, data, ack) => {
|
|
56
|
+
try {
|
|
57
|
+
const parsed = SessionIncrementRequestSchema.parse(data);
|
|
58
|
+
// Get the max session_id for this user + run
|
|
59
|
+
const lastSession = await hubDb.run_session.findFirst({
|
|
60
|
+
select: { session_id: true },
|
|
61
|
+
where: {
|
|
62
|
+
user_id: parsed.userId,
|
|
63
|
+
run_id: parsed.runId,
|
|
64
|
+
},
|
|
65
|
+
orderBy: { session_id: "desc" },
|
|
66
|
+
});
|
|
67
|
+
const newSessionId = lastSession ? lastSession.session_id + 1 : 1;
|
|
68
|
+
const now = new Date().toISOString();
|
|
69
|
+
await hubDb.run_session.create({
|
|
70
|
+
data: {
|
|
71
|
+
user_id: parsed.userId,
|
|
72
|
+
run_id: parsed.runId,
|
|
73
|
+
session_id: newSessionId,
|
|
74
|
+
host_id: hostId,
|
|
75
|
+
model_name: parsed.modelName,
|
|
76
|
+
created_at: now,
|
|
77
|
+
last_active: now,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
ack({ success: true, sessionId: newSessionId });
|
|
81
|
+
pushSessionToSupervisors({
|
|
82
|
+
userId: parsed.userId,
|
|
83
|
+
runId: parsed.runId,
|
|
84
|
+
sessionId: newSessionId,
|
|
85
|
+
modelName: parsed.modelName,
|
|
86
|
+
createdAt: now,
|
|
87
|
+
lastActive: now,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
logService.error(`[Hub:Runs] session_increment error for host ${hostId}: ${error}`);
|
|
92
|
+
ack({ success: false, error: String(error) });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=hubRunService.js.map
|