@naisys/supervisor 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-supervisor +2 -0
- package/client-dist/android-chrome-192x192.png +0 -0
- package/client-dist/android-chrome-512x512.png +0 -0
- package/client-dist/apple-touch-icon.png +0 -0
- package/client-dist/assets/index-BBrK4ItN.js +177 -0
- package/client-dist/assets/index-CKg0vgt5.css +1 -0
- package/client-dist/assets/naisys-logo-CzoPnn5I.webp +0 -0
- package/client-dist/favicon-16x16.png +0 -0
- package/client-dist/favicon-32x32.png +0 -0
- package/client-dist/favicon.ico +0 -0
- package/client-dist/index.html +49 -0
- package/client-dist/site.webmanifest +22 -0
- package/dist/api-reference.js +52 -0
- package/dist/auth-middleware.js +116 -0
- package/dist/database/hubDb.js +26 -0
- package/dist/database/supervisorDb.js +18 -0
- package/dist/error-helpers.js +13 -0
- package/dist/hateoas.js +61 -0
- package/dist/logger.js +11 -0
- package/dist/route-helpers.js +7 -0
- package/dist/routes/admin.js +209 -0
- package/dist/routes/agentChat.js +194 -0
- package/dist/routes/agentConfig.js +265 -0
- package/dist/routes/agentLifecycle.js +350 -0
- package/dist/routes/agentMail.js +171 -0
- package/dist/routes/agentRuns.js +90 -0
- package/dist/routes/agents.js +236 -0
- package/dist/routes/api.js +52 -0
- package/dist/routes/attachments.js +18 -0
- package/dist/routes/auth.js +103 -0
- package/dist/routes/costs.js +51 -0
- package/dist/routes/hosts.js +296 -0
- package/dist/routes/models.js +152 -0
- package/dist/routes/root.js +56 -0
- package/dist/routes/schemas.js +31 -0
- package/dist/routes/status.js +20 -0
- package/dist/routes/users.js +420 -0
- package/dist/routes/variables.js +103 -0
- package/dist/schema-registry.js +23 -0
- package/dist/services/agentConfigService.js +182 -0
- package/dist/services/agentHostStatusService.js +178 -0
- package/dist/services/agentService.js +291 -0
- package/dist/services/attachmentProxyService.js +130 -0
- package/dist/services/browserSocketService.js +78 -0
- package/dist/services/chatService.js +201 -0
- package/dist/services/configExportService.js +61 -0
- package/dist/services/costsService.js +127 -0
- package/dist/services/hostService.js +156 -0
- package/dist/services/hubConnectionService.js +333 -0
- package/dist/services/logFileService.js +11 -0
- package/dist/services/mailService.js +154 -0
- package/dist/services/modelService.js +92 -0
- package/dist/services/runsService.js +164 -0
- package/dist/services/userService.js +147 -0
- package/dist/services/variableService.js +23 -0
- package/dist/supervisorServer.js +221 -0
- package/package.json +79 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { assertUrlSafeKey } from "@naisys/common";
|
|
2
|
+
import { hubDb } from "../database/hubDb.js";
|
|
3
|
+
import { resolveAgentId } from "./agentService.js";
|
|
4
|
+
export async function getHosts() {
|
|
5
|
+
const hosts = await hubDb.hosts.findMany({
|
|
6
|
+
select: {
|
|
7
|
+
id: true,
|
|
8
|
+
name: true,
|
|
9
|
+
restricted: true,
|
|
10
|
+
host_type: true,
|
|
11
|
+
last_active: true,
|
|
12
|
+
_count: {
|
|
13
|
+
select: { user_hosts: true },
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
return hosts.map((host) => ({
|
|
18
|
+
id: host.id,
|
|
19
|
+
name: host.name,
|
|
20
|
+
lastActive: host.last_active?.toISOString() ?? null,
|
|
21
|
+
agentCount: host._count.user_hosts,
|
|
22
|
+
restricted: host.restricted,
|
|
23
|
+
hostType: host.host_type,
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
export async function getHostDetail(hostname) {
|
|
27
|
+
const host = await hubDb.hosts.findUnique({
|
|
28
|
+
where: { name: hostname },
|
|
29
|
+
select: {
|
|
30
|
+
id: true,
|
|
31
|
+
name: true,
|
|
32
|
+
restricted: true,
|
|
33
|
+
host_type: true,
|
|
34
|
+
last_active: true,
|
|
35
|
+
last_ip: true,
|
|
36
|
+
user_hosts: {
|
|
37
|
+
select: {
|
|
38
|
+
users: {
|
|
39
|
+
select: { id: true, username: true, title: true },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
if (!host)
|
|
46
|
+
return null;
|
|
47
|
+
return {
|
|
48
|
+
id: host.id,
|
|
49
|
+
name: host.name,
|
|
50
|
+
lastActive: host.last_active?.toISOString() ?? null,
|
|
51
|
+
lastIp: host.last_ip ?? null,
|
|
52
|
+
restricted: host.restricted,
|
|
53
|
+
hostType: host.host_type,
|
|
54
|
+
online: false, // Caller sets this from agentHostStatusService
|
|
55
|
+
assignedAgents: host.user_hosts.map((uh) => ({
|
|
56
|
+
id: uh.users.id,
|
|
57
|
+
name: uh.users.username,
|
|
58
|
+
title: uh.users.title,
|
|
59
|
+
})),
|
|
60
|
+
_links: [],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export async function createHost(name) {
|
|
64
|
+
assertUrlSafeKey(name, "Host name");
|
|
65
|
+
const existing = await hubDb.hosts.findUnique({ where: { name } });
|
|
66
|
+
if (existing) {
|
|
67
|
+
throw new Error(`Host with name "${name}" already exists`);
|
|
68
|
+
}
|
|
69
|
+
const host = await hubDb.hosts.create({
|
|
70
|
+
data: { name },
|
|
71
|
+
});
|
|
72
|
+
return { id: host.id };
|
|
73
|
+
}
|
|
74
|
+
export async function updateHost(hostname, data) {
|
|
75
|
+
const host = await hubDb.hosts.findUnique({ where: { name: hostname } });
|
|
76
|
+
if (!host) {
|
|
77
|
+
throw new Error(`Host "${hostname}" not found`);
|
|
78
|
+
}
|
|
79
|
+
if (data.name && data.name !== host.name) {
|
|
80
|
+
assertUrlSafeKey(data.name, "Host name");
|
|
81
|
+
const existing = await hubDb.hosts.findUnique({
|
|
82
|
+
where: { name: data.name },
|
|
83
|
+
});
|
|
84
|
+
if (existing) {
|
|
85
|
+
throw new Error(`Host with name "${data.name}" already exists`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
await hubDb.hosts.update({
|
|
89
|
+
where: { name: hostname },
|
|
90
|
+
data: {
|
|
91
|
+
...(data.name !== undefined ? { name: data.name } : {}),
|
|
92
|
+
...(data.restricted !== undefined ? { restricted: data.restricted } : {}),
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
export async function assignAgentToHost(hostname, agentId) {
|
|
97
|
+
const host = await hubDb.hosts.findUnique({ where: { name: hostname } });
|
|
98
|
+
if (!host) {
|
|
99
|
+
throw new Error(`Host "${hostname}" not found`);
|
|
100
|
+
}
|
|
101
|
+
const agent = await hubDb.users.findUnique({ where: { id: agentId } });
|
|
102
|
+
if (!agent) {
|
|
103
|
+
throw new Error(`Agent with ID ${agentId} not found`);
|
|
104
|
+
}
|
|
105
|
+
const existing = await hubDb.user_hosts.findUnique({
|
|
106
|
+
where: { user_id_host_id: { user_id: agentId, host_id: host.id } },
|
|
107
|
+
});
|
|
108
|
+
if (existing) {
|
|
109
|
+
throw new Error("Agent is already assigned to this host");
|
|
110
|
+
}
|
|
111
|
+
await hubDb.user_hosts.create({
|
|
112
|
+
data: { user_id: agentId, host_id: host.id },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
export async function unassignAgentFromHost(hostname, agentName) {
|
|
116
|
+
const host = await hubDb.hosts.findUnique({ where: { name: hostname } });
|
|
117
|
+
if (!host) {
|
|
118
|
+
throw new Error(`Host "${hostname}" not found`);
|
|
119
|
+
}
|
|
120
|
+
const agentId = resolveAgentId(agentName);
|
|
121
|
+
if (!agentId) {
|
|
122
|
+
throw new Error(`Agent "${agentName}" not found`);
|
|
123
|
+
}
|
|
124
|
+
const existing = await hubDb.user_hosts.findUnique({
|
|
125
|
+
where: { user_id_host_id: { user_id: agentId, host_id: host.id } },
|
|
126
|
+
});
|
|
127
|
+
if (!existing) {
|
|
128
|
+
throw new Error("Agent is not assigned to this host");
|
|
129
|
+
}
|
|
130
|
+
await hubDb.user_hosts.delete({
|
|
131
|
+
where: { user_id_host_id: { user_id: agentId, host_id: host.id } },
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
export async function deleteHost(hostname) {
|
|
135
|
+
const host = await hubDb.hosts.findUnique({ where: { name: hostname } });
|
|
136
|
+
if (!host) {
|
|
137
|
+
throw new Error(`Host "${hostname}" not found`);
|
|
138
|
+
}
|
|
139
|
+
const id = host.id;
|
|
140
|
+
await hubDb.$transaction(async (hubTx) => {
|
|
141
|
+
await hubTx.context_log.deleteMany({ where: { host_id: id } });
|
|
142
|
+
await hubTx.costs.deleteMany({ where: { host_id: id } });
|
|
143
|
+
await hubTx.run_session.deleteMany({ where: { host_id: id } });
|
|
144
|
+
await hubTx.mail_messages.updateMany({
|
|
145
|
+
where: { host_id: id },
|
|
146
|
+
data: { host_id: null },
|
|
147
|
+
});
|
|
148
|
+
await hubTx.user_notifications.updateMany({
|
|
149
|
+
where: { latest_host_id: id },
|
|
150
|
+
data: { latest_host_id: null },
|
|
151
|
+
});
|
|
152
|
+
await hubTx.user_hosts.deleteMany({ where: { host_id: id } });
|
|
153
|
+
await hubTx.hosts.delete({ where: { id } });
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=hostService.js.map
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { createPinnedHttpsAgent, parseHubAccessKey, resolveHubAccessKey, verifyHubCertificate, } from "@naisys/common-node";
|
|
2
|
+
import { AgentsStatusSchema, CostPushSchema, HostListSchema, HubEvents, LogPushSchema, MailPushSchema, MailReadPushSchema, SessionPushSchema, } from "@naisys/hub-protocol";
|
|
3
|
+
import { io } from "socket.io-client";
|
|
4
|
+
import { hasPermission } from "../auth-middleware.js";
|
|
5
|
+
import { getLogger } from "../logger.js";
|
|
6
|
+
import { emitAgentsListChanged, emitHubConnectionStatus, markAgentStarted, markAgentStopped, updateAgentsStatus, updateHostsStatus, } from "./agentHostStatusService.js";
|
|
7
|
+
import { refreshUserLookup, resolveUsername } from "./agentService.js";
|
|
8
|
+
import { getIO } from "./browserSocketService.js";
|
|
9
|
+
import { obfuscatePushEntries } from "./runsService.js";
|
|
10
|
+
let socket = null;
|
|
11
|
+
let connected = false;
|
|
12
|
+
let resolvedHubUrl;
|
|
13
|
+
let pinnedAgent = null;
|
|
14
|
+
export function initHubConnection(hubUrl) {
|
|
15
|
+
const hubAccessKey = resolveHubAccessKey();
|
|
16
|
+
resolvedHubUrl = hubUrl;
|
|
17
|
+
if (!hubAccessKey) {
|
|
18
|
+
getLogger().warn("[Supervisor:HubClient] HUB_ACCESS_KEY not set, skipping hub connection");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
getLogger().info(`[Supervisor:HubClient] Connecting to ${hubUrl}...`);
|
|
22
|
+
// Verify the hub's TLS cert fingerprint, then pin all subsequent connections
|
|
23
|
+
// to that exact cert via a custom HTTPS agent
|
|
24
|
+
const { fingerprintPrefix } = parseHubAccessKey(hubAccessKey);
|
|
25
|
+
const parsedUrl = new URL(hubUrl);
|
|
26
|
+
verifyHubCertificate(parsedUrl.hostname, Number(parsedUrl.port) || 443, fingerprintPrefix)
|
|
27
|
+
.then((certPem) => connectSocket(hubUrl, certPem))
|
|
28
|
+
.catch((err) => {
|
|
29
|
+
getLogger().error(`[Supervisor:HubClient] Certificate verification failed: ${err.message}`);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function connectSocket(hubUrl, certPem) {
|
|
33
|
+
pinnedAgent = createPinnedHttpsAgent(certPem);
|
|
34
|
+
socket = io(hubUrl + "/naisys", {
|
|
35
|
+
auth: (cb) => {
|
|
36
|
+
// Re-read access key on each connection attempt so rotated keys are picked up
|
|
37
|
+
cb({
|
|
38
|
+
hubAccessKey: resolveHubAccessKey(),
|
|
39
|
+
hostName: "SUPERVISOR",
|
|
40
|
+
hostType: "supervisor",
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
// Type is narrowed to `string | boolean` for browser compat, but Node accepts an Agent
|
|
44
|
+
agent: pinnedAgent,
|
|
45
|
+
reconnection: true,
|
|
46
|
+
reconnectionDelay: 1000,
|
|
47
|
+
reconnectionDelayMax: 30000,
|
|
48
|
+
});
|
|
49
|
+
socket.on("connect", () => {
|
|
50
|
+
connected = true;
|
|
51
|
+
getLogger().info(`[Supervisor:HubClient] Connected to ${hubUrl}`);
|
|
52
|
+
void refreshUserLookup();
|
|
53
|
+
emitHubConnectionStatus(true);
|
|
54
|
+
});
|
|
55
|
+
socket.on("disconnect", (reason) => {
|
|
56
|
+
connected = false;
|
|
57
|
+
getLogger().info(`[Supervisor:HubClient] Disconnected: ${reason}`);
|
|
58
|
+
emitHubConnectionStatus(false);
|
|
59
|
+
// Server-initiated disconnects don't auto-reconnect in Socket.IO
|
|
60
|
+
if (reason === "io server disconnect") {
|
|
61
|
+
socket?.connect();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
socket.on("connect_error", (error) => {
|
|
65
|
+
getLogger().warn(`[Supervisor:HubClient] Connection error: ${error.message}`);
|
|
66
|
+
});
|
|
67
|
+
socket.on(HubEvents.AGENTS_STATUS, (data) => {
|
|
68
|
+
const parsed = AgentsStatusSchema.safeParse(data);
|
|
69
|
+
if (!parsed.success) {
|
|
70
|
+
getLogger().warn("[Supervisor:HubClient] Invalid agents status: %o", parsed.error);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
updateAgentsStatus(parsed.data.hostActiveAgents, parsed.data.agentNotifications);
|
|
74
|
+
});
|
|
75
|
+
socket.on(HubEvents.HOSTS_UPDATED, (data) => {
|
|
76
|
+
const parsed = HostListSchema.safeParse(data);
|
|
77
|
+
if (!parsed.success) {
|
|
78
|
+
getLogger().warn("[Supervisor:HubClient] Invalid host list: %o", parsed.error);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
updateHostsStatus(parsed.data.hosts);
|
|
82
|
+
});
|
|
83
|
+
socket.on(HubEvents.LOG_PUSH, (data) => {
|
|
84
|
+
const parsed = LogPushSchema.safeParse(data);
|
|
85
|
+
if (!parsed.success) {
|
|
86
|
+
getLogger().warn("[Supervisor:HubClient] Invalid log push: %o", parsed.error);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const browserIO = getIO();
|
|
90
|
+
// Group log entries by session and emit to log rooms
|
|
91
|
+
const bySession = new Map();
|
|
92
|
+
for (const entry of parsed.data.entries) {
|
|
93
|
+
const username = resolveUsername(entry.userId);
|
|
94
|
+
if (!username)
|
|
95
|
+
continue;
|
|
96
|
+
const room = `logs:${username}:${entry.runId}:${entry.sessionId}`;
|
|
97
|
+
if (!bySession.has(room))
|
|
98
|
+
bySession.set(room, []);
|
|
99
|
+
bySession.get(room).push(entry);
|
|
100
|
+
}
|
|
101
|
+
for (const [room, entries] of bySession) {
|
|
102
|
+
// Split broadcast by permission: privileged sockets get full data,
|
|
103
|
+
// unprivileged sockets get obfuscated text and no-access attachments
|
|
104
|
+
const socketIds = browserIO.sockets.adapter.rooms.get(room);
|
|
105
|
+
if (!socketIds || socketIds.size === 0)
|
|
106
|
+
continue;
|
|
107
|
+
let obfuscated;
|
|
108
|
+
for (const socketId of socketIds) {
|
|
109
|
+
const sock = browserIO.sockets.sockets.get(socketId);
|
|
110
|
+
if (!sock)
|
|
111
|
+
continue;
|
|
112
|
+
const user = sock.data.user;
|
|
113
|
+
if (hasPermission(user, "view_run_logs")) {
|
|
114
|
+
sock.emit(room, entries);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
obfuscated ??= obfuscatePushEntries(entries);
|
|
118
|
+
sock.emit(room, obfuscated);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Emit session deltas to runs rooms
|
|
123
|
+
for (const update of parsed.data.sessionUpdates) {
|
|
124
|
+
const username = resolveUsername(update.userId);
|
|
125
|
+
if (!username)
|
|
126
|
+
continue;
|
|
127
|
+
const room = `runs:${username}`;
|
|
128
|
+
browserIO.to(room).emit(room, { type: "log-update", ...update });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
socket.on(HubEvents.COST_PUSH, (data) => {
|
|
132
|
+
const parsed = CostPushSchema.safeParse(data);
|
|
133
|
+
if (!parsed.success) {
|
|
134
|
+
getLogger().warn("[Supervisor:HubClient] Invalid cost push: %o", parsed.error);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const browserIO = getIO();
|
|
138
|
+
for (const entry of parsed.data.entries) {
|
|
139
|
+
const username = resolveUsername(entry.userId);
|
|
140
|
+
if (!username)
|
|
141
|
+
continue;
|
|
142
|
+
const room = `runs:${username}`;
|
|
143
|
+
browserIO.to(room).emit(room, { type: "cost-update", ...entry });
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
socket.on(HubEvents.SESSION_PUSH, (data) => {
|
|
147
|
+
const parsed = SessionPushSchema.safeParse(data);
|
|
148
|
+
if (!parsed.success) {
|
|
149
|
+
getLogger().warn("[Supervisor:HubClient] Invalid session push: %o", parsed.error);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const { session } = parsed.data;
|
|
153
|
+
const username = resolveUsername(session.userId);
|
|
154
|
+
if (!username)
|
|
155
|
+
return;
|
|
156
|
+
const browserIO = getIO();
|
|
157
|
+
const room = `runs:${username}`;
|
|
158
|
+
browserIO.to(room).emit(room, { type: "new-session", ...session });
|
|
159
|
+
});
|
|
160
|
+
// Track last pushed message ID per room for gap detection
|
|
161
|
+
const lastPushedMessageId = new Map();
|
|
162
|
+
socket.on(HubEvents.MAIL_PUSH, (data) => {
|
|
163
|
+
const parsed = MailPushSchema.safeParse(data);
|
|
164
|
+
if (!parsed.success) {
|
|
165
|
+
getLogger().warn("[Supervisor:HubClient] Invalid mail push: %o", parsed.error);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const msg = parsed.data;
|
|
169
|
+
const affectedUserIds = new Set([...msg.recipientUserIds, msg.fromUserId]);
|
|
170
|
+
const browserIO = getIO();
|
|
171
|
+
if (msg.kind === "mail") {
|
|
172
|
+
for (const uid of affectedUserIds) {
|
|
173
|
+
const username = resolveUsername(uid);
|
|
174
|
+
if (!username)
|
|
175
|
+
continue;
|
|
176
|
+
const room = `mail:${username}`;
|
|
177
|
+
const previousMessageId = lastPushedMessageId.get(room) ?? null;
|
|
178
|
+
browserIO
|
|
179
|
+
.to(room)
|
|
180
|
+
.emit(room, { type: "new-message", previousMessageId, ...msg });
|
|
181
|
+
lastPushedMessageId.set(room, msg.messageId);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else if (msg.kind === "chat") {
|
|
185
|
+
// Chat messages — room keyed by participants (not user-specific)
|
|
186
|
+
const msgRoom = `chat-messages:${msg.participants}`;
|
|
187
|
+
const previousMessageId = lastPushedMessageId.get(msgRoom) ?? null;
|
|
188
|
+
browserIO
|
|
189
|
+
.to(msgRoom)
|
|
190
|
+
.emit(msgRoom, { type: "new-message", previousMessageId, ...msg });
|
|
191
|
+
lastPushedMessageId.set(msgRoom, msg.messageId);
|
|
192
|
+
// Chat conversations — rooms keyed by username (no gap tracking needed here)
|
|
193
|
+
const convPayload = {
|
|
194
|
+
type: "new-message",
|
|
195
|
+
previousMessageId: null,
|
|
196
|
+
...msg,
|
|
197
|
+
};
|
|
198
|
+
for (const uid of affectedUserIds) {
|
|
199
|
+
const username = resolveUsername(uid);
|
|
200
|
+
if (!username)
|
|
201
|
+
continue;
|
|
202
|
+
const convRoom = `chat-conversations:${username}`;
|
|
203
|
+
browserIO.to(convRoom).emit(convRoom, convPayload);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
socket.on(HubEvents.MAIL_READ_PUSH, (data) => {
|
|
208
|
+
const parsed = MailReadPushSchema.safeParse(data);
|
|
209
|
+
if (!parsed.success) {
|
|
210
|
+
getLogger().warn("[Supervisor:HubClient] Invalid mail read push: %o", parsed.error);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const msg = parsed.data;
|
|
214
|
+
const receipt = {
|
|
215
|
+
type: "read-receipt",
|
|
216
|
+
messageIds: msg.messageIds,
|
|
217
|
+
userId: msg.userId,
|
|
218
|
+
};
|
|
219
|
+
if (msg.kind === "mail") {
|
|
220
|
+
// Collect all unique participant usernames
|
|
221
|
+
const participantUsernames = new Set(msg.participants.flatMap((p) => p.split(",")));
|
|
222
|
+
const browserIO = getIO();
|
|
223
|
+
for (const name of participantUsernames) {
|
|
224
|
+
const room = `mail:${name}`;
|
|
225
|
+
browserIO.to(room).emit(room, receipt);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else if (msg.kind === "chat") {
|
|
229
|
+
// participants here is the room key
|
|
230
|
+
const browserIO = getIO();
|
|
231
|
+
for (const p of msg.participants) {
|
|
232
|
+
const room = `chat-messages:${p}`;
|
|
233
|
+
browserIO.to(room).emit(room, receipt);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
// User list changed (hub broadcasts after create/edit/archive/delete)
|
|
238
|
+
socket.on(HubEvents.USERS_UPDATED, () => {
|
|
239
|
+
void refreshUserLookup();
|
|
240
|
+
emitAgentsListChanged();
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
export function isHubConnected() {
|
|
244
|
+
return connected;
|
|
245
|
+
}
|
|
246
|
+
export function getHubAccessKey() {
|
|
247
|
+
return resolveHubAccessKey();
|
|
248
|
+
}
|
|
249
|
+
export function getHubUrl() {
|
|
250
|
+
return resolvedHubUrl;
|
|
251
|
+
}
|
|
252
|
+
export function getHubPinnedAgent() {
|
|
253
|
+
return pinnedAgent;
|
|
254
|
+
}
|
|
255
|
+
export function sendAgentStart(startUserId, taskDescription, requesterUserId) {
|
|
256
|
+
return new Promise((resolve, reject) => {
|
|
257
|
+
if (!socket || !connected) {
|
|
258
|
+
reject(new Error("Not connected to hub"));
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
socket.emit(HubEvents.AGENT_START, { startUserId, taskDescription, requesterUserId }, (response) => {
|
|
262
|
+
if (response.success) {
|
|
263
|
+
markAgentStarted(startUserId);
|
|
264
|
+
}
|
|
265
|
+
resolve(response);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
export function sendMailViaHub(fromUserId, toUserIds, subject, body, kind = "mail", attachmentIds) {
|
|
270
|
+
return new Promise((resolve, reject) => {
|
|
271
|
+
if (!socket || !connected) {
|
|
272
|
+
reject(new Error("Not connected to hub"));
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
socket.emit(HubEvents.MAIL_SEND, { fromUserId, toUserIds, subject, body, kind, attachmentIds }, (response) => {
|
|
276
|
+
resolve(response);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
export function sendUserListChanged() {
|
|
281
|
+
if (!socket || !connected) {
|
|
282
|
+
getLogger().warn("[Supervisor:HubClient] Not connected to hub, cannot send user list changed");
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
socket.emit(HubEvents.USERS_CHANGED);
|
|
286
|
+
}
|
|
287
|
+
export function sendModelsChanged() {
|
|
288
|
+
if (!socket || !connected) {
|
|
289
|
+
getLogger().warn("[Supervisor:HubClient] Not connected to hub, cannot send models changed");
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
socket.emit(HubEvents.MODELS_CHANGED);
|
|
293
|
+
}
|
|
294
|
+
export function sendVariablesChanged() {
|
|
295
|
+
if (!socket || !connected) {
|
|
296
|
+
getLogger().warn("[Supervisor:HubClient] Not connected to hub, cannot send variables changed");
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
socket.emit(HubEvents.VARIABLES_CHANGED);
|
|
300
|
+
}
|
|
301
|
+
export function sendHostsChanged() {
|
|
302
|
+
if (!socket || !connected) {
|
|
303
|
+
getLogger().warn("[Supervisor:HubClient] Not connected to hub, cannot send hosts changed");
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
socket.emit(HubEvents.HOSTS_CHANGED);
|
|
307
|
+
}
|
|
308
|
+
export function sendRotateAccessKey() {
|
|
309
|
+
return new Promise((resolve, reject) => {
|
|
310
|
+
if (!socket || !connected) {
|
|
311
|
+
reject(new Error("Not connected to hub"));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
socket.emit(HubEvents.ROTATE_ACCESS_KEY, {}, (response) => {
|
|
315
|
+
resolve(response);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
export function sendAgentStop(userId, reason) {
|
|
320
|
+
return new Promise((resolve, reject) => {
|
|
321
|
+
if (!socket || !connected) {
|
|
322
|
+
reject(new Error("Not connected to hub"));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
socket.emit(HubEvents.AGENT_STOP, { userId, reason }, (response) => {
|
|
326
|
+
if (response.success) {
|
|
327
|
+
markAgentStopped(userId);
|
|
328
|
+
}
|
|
329
|
+
resolve(response);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
//# sourceMappingURL=hubConnectionService.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { tailLogFile } from "@naisys/common-node";
|
|
3
|
+
export { tailLogFile };
|
|
4
|
+
export function getLogFilePath(fileKey) {
|
|
5
|
+
const naisysFolder = process.env.NAISYS_FOLDER;
|
|
6
|
+
if (!naisysFolder) {
|
|
7
|
+
throw new Error("NAISYS_FOLDER environment variable is not set.");
|
|
8
|
+
}
|
|
9
|
+
return path.join(naisysFolder, "logs", `${fileKey}.log`);
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=logFileService.js.map
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { hubDb } from "../database/hubDb.js";
|
|
2
|
+
import { getLogger } from "../logger.js";
|
|
3
|
+
import { uploadToHub } from "./attachmentProxyService.js";
|
|
4
|
+
import { sendMailViaHub } from "./hubConnectionService.js";
|
|
5
|
+
/**
|
|
6
|
+
* Get mail data for a specific agent by userId, optionally filtering by updatedSince
|
|
7
|
+
*/
|
|
8
|
+
export async function getMailDataByUserId(userId, updatedSince, page = 1, count = 50, kind = "mail") {
|
|
9
|
+
// Build the where clause
|
|
10
|
+
const whereClause = { kind };
|
|
11
|
+
// If updatedSince is provided, filter by date
|
|
12
|
+
if (updatedSince) {
|
|
13
|
+
whereClause.created_at = { gte: updatedSince };
|
|
14
|
+
}
|
|
15
|
+
const where = {
|
|
16
|
+
...whereClause,
|
|
17
|
+
OR: [
|
|
18
|
+
{ from_user_id: userId },
|
|
19
|
+
{
|
|
20
|
+
recipients: {
|
|
21
|
+
some: {
|
|
22
|
+
user_id: userId,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
// Only get total count on initial fetch (when updatedSince is not set)
|
|
29
|
+
const total = updatedSince
|
|
30
|
+
? undefined
|
|
31
|
+
: await hubDb.mail_messages.count({ where });
|
|
32
|
+
// Get paginated messages
|
|
33
|
+
const dbMessages = await hubDb.mail_messages.findMany({
|
|
34
|
+
where,
|
|
35
|
+
orderBy: { id: "desc" },
|
|
36
|
+
skip: (page - 1) * count,
|
|
37
|
+
take: count,
|
|
38
|
+
select: {
|
|
39
|
+
id: true,
|
|
40
|
+
from_user_id: true,
|
|
41
|
+
subject: true,
|
|
42
|
+
body: true,
|
|
43
|
+
created_at: true,
|
|
44
|
+
from_user: {
|
|
45
|
+
select: { username: true, title: true },
|
|
46
|
+
},
|
|
47
|
+
recipients: {
|
|
48
|
+
select: {
|
|
49
|
+
user_id: true,
|
|
50
|
+
type: true,
|
|
51
|
+
read_at: true,
|
|
52
|
+
archived_at: true,
|
|
53
|
+
user: {
|
|
54
|
+
select: { username: true, title: true },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
mail_attachments: {
|
|
59
|
+
include: {
|
|
60
|
+
attachment: {
|
|
61
|
+
select: { public_id: true, filename: true, file_size: true },
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
const messages = dbMessages.map((msg) => ({
|
|
68
|
+
id: msg.id,
|
|
69
|
+
fromUserId: msg.from_user_id,
|
|
70
|
+
fromUsername: msg.from_user.username,
|
|
71
|
+
fromTitle: msg.from_user.title,
|
|
72
|
+
subject: msg.subject,
|
|
73
|
+
body: msg.body,
|
|
74
|
+
createdAt: msg.created_at.toISOString(),
|
|
75
|
+
recipients: msg.recipients.map((r) => ({
|
|
76
|
+
userId: r.user_id,
|
|
77
|
+
username: r.user.username,
|
|
78
|
+
title: r.user.title,
|
|
79
|
+
type: r.type,
|
|
80
|
+
readAt: r.read_at?.toISOString() ?? null,
|
|
81
|
+
archivedAt: r.archived_at?.toISOString() ?? null,
|
|
82
|
+
})),
|
|
83
|
+
attachments: msg.mail_attachments.length > 0
|
|
84
|
+
? msg.mail_attachments.map((ma) => ({
|
|
85
|
+
id: ma.attachment.public_id,
|
|
86
|
+
filename: ma.attachment.filename,
|
|
87
|
+
fileSize: ma.attachment.file_size,
|
|
88
|
+
}))
|
|
89
|
+
: undefined,
|
|
90
|
+
}));
|
|
91
|
+
return {
|
|
92
|
+
mail: messages,
|
|
93
|
+
timestamp: new Date().toISOString(),
|
|
94
|
+
total,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Archive all mail messages where the user is a recipient
|
|
99
|
+
*/
|
|
100
|
+
export async function archiveAllMailMessages(userId) {
|
|
101
|
+
const result = await hubDb.mail_recipients.updateMany({
|
|
102
|
+
where: {
|
|
103
|
+
user_id: userId,
|
|
104
|
+
archived_at: null,
|
|
105
|
+
message: {
|
|
106
|
+
kind: "mail",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
data: {
|
|
110
|
+
archived_at: new Date(),
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
return result.count;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Send a message via the hub, uploading any attachments first
|
|
117
|
+
*/
|
|
118
|
+
export async function sendMessage(request, attachments) {
|
|
119
|
+
try {
|
|
120
|
+
const { fromId, toIds, subject, message } = request;
|
|
121
|
+
// Clean message (handle escaped newlines)
|
|
122
|
+
const cleanMessage = message.replace(/\\n/g, "\n");
|
|
123
|
+
// Upload attachments to hub and collect IDs
|
|
124
|
+
let attachmentIds;
|
|
125
|
+
if (attachments && attachments.length > 0) {
|
|
126
|
+
attachmentIds = [];
|
|
127
|
+
for (const attachment of attachments) {
|
|
128
|
+
const id = await uploadToHub(attachment.data, attachment.filename, fromId, "mail");
|
|
129
|
+
attachmentIds.push(id);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const response = await sendMailViaHub(fromId, toIds, subject, cleanMessage, "mail", attachmentIds);
|
|
133
|
+
if (response.success) {
|
|
134
|
+
return {
|
|
135
|
+
success: true,
|
|
136
|
+
message: "Message sent successfully",
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
return {
|
|
141
|
+
success: false,
|
|
142
|
+
message: response.error || "Failed to send message",
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
getLogger().error(error, "Error sending message");
|
|
148
|
+
return {
|
|
149
|
+
success: false,
|
|
150
|
+
message: error instanceof Error ? error.message : "Failed to send message",
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=mailService.js.map
|