agent-relay-server 0.3.11 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +237 -22
- package/bin/agent-relay-codex.ts +79 -6
- package/codex/README.md +18 -3
- package/codex/hooks/session-start.ts +2 -2
- package/codex/live-sidecar.ts +2 -0
- package/codex/plugin/.codex-plugin/plugin.json +1 -1
- package/codex/plugin/skills/agent-relay/SKILL.md +1 -0
- package/codex/relay.ts +8 -3
- package/examples/integrations/github-issue.ts +54 -0
- package/examples/integrations/ops-alert.sh +27 -0
- package/examples/integrations/prometheus-alertmanager.ts +61 -0
- package/examples/integrations/support-ticket.sh +28 -0
- package/package.json +5 -4
- package/public/dashboard.js +701 -0
- package/public/index.html +143 -504
- package/src/cli.ts +217 -0
- package/src/config.ts +38 -0
- package/src/daemon.ts +453 -0
- package/src/db.ts +442 -16
- package/src/index.ts +96 -70
- package/src/routes.ts +334 -17
- package/src/security.ts +103 -0
- package/src/setup.ts +187 -0
- package/src/sse.ts +18 -2
- package/src/types.ts +67 -1
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const PREF_PREFIX = "ar-";
|
|
3
|
+
const DEFAULT_COMPOSE = { from: "", to: "", body: "", channel: "", subject: "", claimable: false };
|
|
4
|
+
const CLOSED_TASK_STATUSES = new Set(["done", "failed", "canceled"]);
|
|
5
|
+
const STATUS_SORT_ORDER = { online: 0, idle: 1, busy: 2, offline: 3 };
|
|
6
|
+
|
|
7
|
+
function loadPref(key, fallback) {
|
|
8
|
+
try {
|
|
9
|
+
const value = localStorage.getItem(PREF_PREFIX + key);
|
|
10
|
+
return value !== null ? JSON.parse(value) : fallback;
|
|
11
|
+
} catch {
|
|
12
|
+
return fallback;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function savePref(key, value) {
|
|
17
|
+
localStorage.setItem(PREF_PREFIX + key, JSON.stringify(value));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function initialState() {
|
|
21
|
+
return {
|
|
22
|
+
view: loadPref("view", "overview"),
|
|
23
|
+
|
|
24
|
+
showOffline: loadPref("showOffline", false),
|
|
25
|
+
autoRefresh: loadPref("autoRefresh", true),
|
|
26
|
+
agentSort: loadPref("agentSort", "status"),
|
|
27
|
+
agentSortDir: loadPref("agentSortDir", "asc"),
|
|
28
|
+
|
|
29
|
+
agents: [],
|
|
30
|
+
agentsById: {},
|
|
31
|
+
messages: [],
|
|
32
|
+
tasks: [],
|
|
33
|
+
taskEvents: [],
|
|
34
|
+
stats: {},
|
|
35
|
+
authToken: loadPref("authToken", ""),
|
|
36
|
+
|
|
37
|
+
selectedAgent: "",
|
|
38
|
+
replyTo: null,
|
|
39
|
+
composeOpen: false,
|
|
40
|
+
threadOpen: false,
|
|
41
|
+
threadMessages: [],
|
|
42
|
+
taskEventsOpen: false,
|
|
43
|
+
connected: false,
|
|
44
|
+
authNeeded: false,
|
|
45
|
+
|
|
46
|
+
compose: { ...DEFAULT_COMPOSE },
|
|
47
|
+
|
|
48
|
+
confirmModal: { show: false, title: "", message: "", action: null },
|
|
49
|
+
renameModal: { show: false, agentId: "", label: "" },
|
|
50
|
+
|
|
51
|
+
channelFilter: "",
|
|
52
|
+
tagFilter: "",
|
|
53
|
+
agentStatusFilter: loadPref("agentStatusFilter", ""),
|
|
54
|
+
agentTagFilter: loadPref("agentTagFilter", ""),
|
|
55
|
+
taskStatusFilter: "",
|
|
56
|
+
taskSourceFilter: "",
|
|
57
|
+
|
|
58
|
+
chartInstances: {},
|
|
59
|
+
_es: null,
|
|
60
|
+
_statsTimer: null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function watchPersistedPrefs(vm) {
|
|
65
|
+
vm.$watch("showOffline", (value) => vm.save("showOffline", value));
|
|
66
|
+
vm.$watch("agentSort", (value) => vm.save("agentSort", value));
|
|
67
|
+
vm.$watch("agentSortDir", (value) => vm.save("agentSortDir", value));
|
|
68
|
+
vm.$watch("agentStatusFilter", (value) => vm.save("agentStatusFilter", value));
|
|
69
|
+
vm.$watch("agentTagFilter", (value) => vm.save("agentTagFilter", value));
|
|
70
|
+
vm.$watch("view", (value) => {
|
|
71
|
+
vm.save("view", value);
|
|
72
|
+
if (value === "analytics") vm.$nextTick(() => vm.renderCharts());
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parseEventData(event) {
|
|
77
|
+
return JSON.parse(event.data);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function indexAgents(agents) {
|
|
81
|
+
const byId = {};
|
|
82
|
+
for (const agent of agents) byId[agent.id] = agent;
|
|
83
|
+
return byId;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function upsertById(list, item) {
|
|
87
|
+
const idx = list.findIndex((existing) => existing.id === item.id);
|
|
88
|
+
if (idx >= 0) list[idx] = item;
|
|
89
|
+
else list.push(item);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function upsertTask(vm, task) {
|
|
93
|
+
const idx = vm.tasks.findIndex((existing) => existing.id === task.id);
|
|
94
|
+
if (idx >= 0) vm.tasks[idx] = task;
|
|
95
|
+
else vm.tasks.unshift(task);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function createLifecycleMethods() {
|
|
99
|
+
return {
|
|
100
|
+
async init() {
|
|
101
|
+
await this.refresh();
|
|
102
|
+
this.connectSSE();
|
|
103
|
+
this._statsTimer = setInterval(() => this.fetchStats(), 30_000);
|
|
104
|
+
watchPersistedPrefs(this);
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
save(key, value) {
|
|
108
|
+
savePref(key, value);
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
switchView(view) {
|
|
112
|
+
this.view = view;
|
|
113
|
+
if (view === "messages") this.fetchMessages();
|
|
114
|
+
if (view === "tasks") this.fetchTasks();
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function createSseMethods() {
|
|
120
|
+
return {
|
|
121
|
+
connectSSE,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function connectSSE() {
|
|
126
|
+
if (this._es) this._es.close();
|
|
127
|
+
|
|
128
|
+
const es = new EventSource(buildEventsUrl(this.authToken));
|
|
129
|
+
this._es = es;
|
|
130
|
+
|
|
131
|
+
es.addEventListener("connected", () => {
|
|
132
|
+
this.connected = true;
|
|
133
|
+
});
|
|
134
|
+
es.onerror = () => {
|
|
135
|
+
this.connected = false;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
es.addEventListener("message.new", (event) => handleNewMessage(this, parseEventData(event)));
|
|
139
|
+
es.addEventListener("agent.status", (event) => handleAgentStatus(this, parseEventData(event)));
|
|
140
|
+
es.addEventListener("agent.removed", (event) => handleAgentRemoved(this, parseEventData(event)));
|
|
141
|
+
es.addEventListener("message.claimed", (event) => handleMessageClaimed(this, parseEventData(event)));
|
|
142
|
+
es.addEventListener("message.deleted", (event) => handleMessageDeleted(this, parseEventData(event)));
|
|
143
|
+
registerTaskEvents(this, es);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function buildEventsUrl(authToken) {
|
|
147
|
+
const eventUrl = new URL(window.location.origin + "/api/events");
|
|
148
|
+
if (authToken) eventUrl.searchParams.set("token", authToken);
|
|
149
|
+
return eventUrl.toString();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function handleNewMessage(vm, msg) {
|
|
153
|
+
if (vm.messages.some((existing) => existing.id === msg.id)) return;
|
|
154
|
+
if (vm.selectedAgent && msg.from !== vm.selectedAgent && msg.to !== vm.selectedAgent) return;
|
|
155
|
+
if (vm.channelFilter && msg.channel !== vm.channelFilter) return;
|
|
156
|
+
|
|
157
|
+
vm.messages.push(msg);
|
|
158
|
+
if (vm.messages.length > 200) vm.messages.shift();
|
|
159
|
+
vm.stats.messages = (vm.stats.messages ?? 0) + 1;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function handleAgentStatus(vm, agent) {
|
|
163
|
+
upsertById(vm.agents, agent);
|
|
164
|
+
vm.agentsById[agent.id] = agent;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function handleAgentRemoved(vm, data) {
|
|
168
|
+
vm.agents = vm.agents.filter((agent) => agent.id !== data.id);
|
|
169
|
+
delete vm.agentsById[data.id];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function handleMessageClaimed(vm, data) {
|
|
173
|
+
const msg = vm.messages.find((item) => item.id === data.messageId);
|
|
174
|
+
if (msg) msg.claimedBy = data.claimedBy;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function handleMessageDeleted(vm, data) {
|
|
178
|
+
vm.messages = vm.messages.filter((msg) => msg.id !== data.messageId);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function registerTaskEvents(vm, es) {
|
|
182
|
+
for (const eventName of ["task.created", "task.updated", "task.claimed", "task.status"]) {
|
|
183
|
+
es.addEventListener(eventName, (event) => upsertTask(vm, parseEventData(event)));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function createApiMethods() {
|
|
188
|
+
return {
|
|
189
|
+
async api(method, path, body) {
|
|
190
|
+
const opts = { method, headers: {} };
|
|
191
|
+
if (this.authToken) opts.headers["X-Agent-Relay-Token"] = this.authToken;
|
|
192
|
+
if (body) {
|
|
193
|
+
opts.headers["Content-Type"] = "application/json";
|
|
194
|
+
opts.body = JSON.stringify(body);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const response = await fetch(window.location.origin + "/api" + path, opts);
|
|
198
|
+
if (!response.ok) {
|
|
199
|
+
if (response.status === 401) this.authNeeded = true;
|
|
200
|
+
const text = await response.text();
|
|
201
|
+
throw new Error(text || response.statusText);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
this.authNeeded = false;
|
|
205
|
+
return response.json();
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
saveTokenAndRefresh() {
|
|
209
|
+
this.save("authToken", this.authToken);
|
|
210
|
+
this.authNeeded = false;
|
|
211
|
+
this.connectSSE();
|
|
212
|
+
this.refresh();
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
async refresh() {
|
|
216
|
+
await Promise.all([this.fetchStats(), this.fetchAgents(), this.fetchMessages(), this.fetchTasks()]);
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
async fetchStats() {
|
|
220
|
+
try {
|
|
221
|
+
this.stats = await this.api("GET", "/stats");
|
|
222
|
+
} catch {}
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
async fetchAgents() {
|
|
226
|
+
try {
|
|
227
|
+
this.agents = await this.api("GET", "/agents");
|
|
228
|
+
this.agentsById = indexAgents(this.agents);
|
|
229
|
+
} catch {}
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
async fetchMessages() {
|
|
233
|
+
try {
|
|
234
|
+
let path = "/messages?limit=100";
|
|
235
|
+
if (this.selectedAgent) path += "&for=" + encodeURIComponent(this.selectedAgent);
|
|
236
|
+
if (this.channelFilter) path += "&channel=" + encodeURIComponent(this.channelFilter);
|
|
237
|
+
this.messages = await this.api("GET", path);
|
|
238
|
+
} catch {}
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
async fetchTasks() {
|
|
242
|
+
try {
|
|
243
|
+
const params = new URLSearchParams({ limit: "100" });
|
|
244
|
+
if (this.taskStatusFilter) params.set("status", this.taskStatusFilter);
|
|
245
|
+
if (this.taskSourceFilter) params.set("source", this.taskSourceFilter);
|
|
246
|
+
this.tasks = await this.api("GET", "/tasks?" + params.toString());
|
|
247
|
+
} catch {}
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function createComputedDescriptors() {
|
|
253
|
+
return {
|
|
254
|
+
onlineCount: { get: getOnlineCount },
|
|
255
|
+
sortedAgents: { get: getSortedAgents },
|
|
256
|
+
filteredMessages: { get: getFilteredMessages },
|
|
257
|
+
groupedMessages: { get: getGroupedMessages },
|
|
258
|
+
filteredTasks: { get: getFilteredTasks },
|
|
259
|
+
composeAgents: { get: getComposeAgents },
|
|
260
|
+
uniqueLabels: { get: getUniqueLabels },
|
|
261
|
+
uniqueCaps: { get: getUniqueCaps },
|
|
262
|
+
uniqueTags: { get: getUniqueTags },
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function getOnlineCount() {
|
|
267
|
+
return this.agents.filter((agent) => agent.status !== "offline").length;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function getSortedAgents() {
|
|
271
|
+
let list = this.showOffline ? [...this.agents] : this.agents.filter((agent) => agent.status !== "offline");
|
|
272
|
+
if (this.agentStatusFilter === "starting") {
|
|
273
|
+
list = list.filter((agent) => agent.status !== "offline" && !agent.ready);
|
|
274
|
+
} else if (this.agentStatusFilter) {
|
|
275
|
+
list = list.filter((agent) => agent.status === this.agentStatusFilter);
|
|
276
|
+
}
|
|
277
|
+
if (this.agentTagFilter) {
|
|
278
|
+
list = list.filter((agent) => (agent.tags || []).includes(this.agentTagFilter));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const dir = this.agentSortDir === "desc" ? -1 : 1;
|
|
282
|
+
return list.sort((a, b) => compareAgents(this, a, b) * dir);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function getFilteredMessages() {
|
|
286
|
+
if (!this.tagFilter) return this.messages;
|
|
287
|
+
return this.messages.filter((msg) => messageMatchesTag(this, msg, this.tagFilter));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function messageMatchesTag(vm, msg, tag) {
|
|
291
|
+
if (msg.to === "tag:" + tag) return true;
|
|
292
|
+
if (vm.agentsById[msg.from]?.tags?.includes(tag)) return true;
|
|
293
|
+
if (vm.agentsById[msg.to]?.tags?.includes(tag)) return true;
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function getGroupedMessages() {
|
|
298
|
+
const threads = new Map();
|
|
299
|
+
for (const msg of this.filteredMessages) {
|
|
300
|
+
const threadId = msg.threadId || msg.id;
|
|
301
|
+
if (!threads.has(threadId)) threads.set(threadId, { threadId, messages: [] });
|
|
302
|
+
threads.get(threadId).messages.push(msg);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
for (const group of threads.values()) {
|
|
306
|
+
group.messages.sort((a, b) => a.id - b.id);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return [...threads.values()].sort(compareThreadGroups);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function compareThreadGroups(a, b) {
|
|
313
|
+
const aLast = a.messages[a.messages.length - 1].id;
|
|
314
|
+
const bLast = b.messages[b.messages.length - 1].id;
|
|
315
|
+
return bLast - aLast;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function getFilteredTasks() {
|
|
319
|
+
if (this.taskStatusFilter) return this.tasks;
|
|
320
|
+
return this.tasks.filter((task) => !CLOSED_TASK_STATUSES.has(task.status));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function getComposeAgents() {
|
|
324
|
+
return this.showOffline ? this.agents : this.agents.filter((agent) => agent.status !== "offline");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function getUniqueLabels() {
|
|
328
|
+
return [...new Set(this.agents.filter((agent) => agent.label).map((agent) => agent.label))];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function getUniqueCaps() {
|
|
332
|
+
return [...new Set(this.agents.flatMap((agent) => agent.capabilities || []))];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function getUniqueTags() {
|
|
336
|
+
return [...new Set(this.agents.flatMap((agent) => agent.tags || []))];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function compareAgents(vm, a, b) {
|
|
340
|
+
switch (vm.agentSort) {
|
|
341
|
+
case "name":
|
|
342
|
+
return vm.displayName(a).localeCompare(vm.displayName(b));
|
|
343
|
+
case "status":
|
|
344
|
+
return (STATUS_SORT_ORDER[a.status] ?? 9) - (STATUS_SORT_ORDER[b.status] ?? 9);
|
|
345
|
+
case "lastSeen":
|
|
346
|
+
return new Date(b.lastSeen) - new Date(a.lastSeen);
|
|
347
|
+
case "created":
|
|
348
|
+
return new Date(b.createdAt) - new Date(a.createdAt);
|
|
349
|
+
default:
|
|
350
|
+
return 0;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function createDisplayMethods() {
|
|
355
|
+
return {
|
|
356
|
+
displayName,
|
|
357
|
+
displayTarget,
|
|
358
|
+
severityClass,
|
|
359
|
+
agentStatusTitle,
|
|
360
|
+
timeAgo,
|
|
361
|
+
fmtTime,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function displayName(agent) {
|
|
366
|
+
if (!agent) return "?";
|
|
367
|
+
return agent.label || agent.name || agent.id.slice(-12);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function displayTarget(target) {
|
|
371
|
+
if (!target) return "?";
|
|
372
|
+
if (target === "broadcast") return "broadcast";
|
|
373
|
+
if (target.startsWith("tag:")) return "#" + target.slice(4);
|
|
374
|
+
if (target.startsWith("cap:")) return target.slice(4);
|
|
375
|
+
if (target.startsWith("label:")) return target.slice(6);
|
|
376
|
+
|
|
377
|
+
const agent = this.agentsById[target];
|
|
378
|
+
return agent ? this.displayName(agent) : target.slice(-8);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function severityClass(severity) {
|
|
382
|
+
if (severity === "critical") return "bg-danger-lt";
|
|
383
|
+
if (severity === "warning") return "bg-warning-lt";
|
|
384
|
+
return "bg-info-lt";
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function agentStatusTitle(agent) {
|
|
388
|
+
if (!agent) return "";
|
|
389
|
+
if (agent.status === "offline") return "offline";
|
|
390
|
+
if (agent.ready) return agent.status;
|
|
391
|
+
|
|
392
|
+
const lastSeenMs = new Date(agent.lastSeen).getTime();
|
|
393
|
+
if (!Number.isFinite(lastSeenMs)) return "Trying to reconnect...";
|
|
394
|
+
|
|
395
|
+
const ageSec = Math.max(0, (Date.now() - lastSeenMs) / 1000);
|
|
396
|
+
return ageSec <= 45 ? "Starting up..." : "Trying to reconnect...";
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function timeAgo(iso) {
|
|
400
|
+
if (!iso) return "";
|
|
401
|
+
const ts = new Date(iso).getTime();
|
|
402
|
+
if (!Number.isFinite(ts)) return "";
|
|
403
|
+
|
|
404
|
+
const diff = Math.max(0, (Date.now() - ts) / 1000);
|
|
405
|
+
if (diff < 60) return Math.floor(diff) + "s ago";
|
|
406
|
+
if (diff < 3600) return Math.floor(diff / 60) + "m ago";
|
|
407
|
+
if (diff < 86400) return Math.floor(diff / 3600) + "h ago";
|
|
408
|
+
return Math.floor(diff / 86400) + "d ago";
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function fmtTime(iso) {
|
|
412
|
+
if (!iso) return "";
|
|
413
|
+
return new Date(iso).toLocaleString();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function createMessageActions() {
|
|
417
|
+
return {
|
|
418
|
+
openCompose,
|
|
419
|
+
openComposeToAgent,
|
|
420
|
+
startReply,
|
|
421
|
+
cancelReply,
|
|
422
|
+
doSend,
|
|
423
|
+
doClaim,
|
|
424
|
+
doDeleteMessage,
|
|
425
|
+
openThread,
|
|
426
|
+
openTaskEvents,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function focusComposeBody(vm) {
|
|
431
|
+
vm.$nextTick(() => vm.$refs.composeBody?.focus());
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function openCompose() {
|
|
435
|
+
if (!this.replyTo) this.compose = { ...DEFAULT_COMPOSE, from: "user" };
|
|
436
|
+
this.composeOpen = true;
|
|
437
|
+
focusComposeBody(this);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function openComposeToAgent(agent) {
|
|
441
|
+
this.replyTo = null;
|
|
442
|
+
this.compose = { ...DEFAULT_COMPOSE, from: "user", to: agent.id };
|
|
443
|
+
this.composeOpen = true;
|
|
444
|
+
focusComposeBody(this);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function startReply(msg) {
|
|
448
|
+
this.replyTo = { id: msg.id, from: msg.from };
|
|
449
|
+
this.compose = {
|
|
450
|
+
...DEFAULT_COMPOSE,
|
|
451
|
+
from: "",
|
|
452
|
+
to: msg.from,
|
|
453
|
+
channel: msg.channel || "",
|
|
454
|
+
};
|
|
455
|
+
this.openCompose();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function cancelReply() {
|
|
459
|
+
this.replyTo = null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function buildMessagePayload(vm) {
|
|
463
|
+
const payload = {
|
|
464
|
+
from: vm.compose.from,
|
|
465
|
+
to: vm.compose.to,
|
|
466
|
+
body: vm.compose.body,
|
|
467
|
+
};
|
|
468
|
+
if (vm.compose.channel) payload.channel = vm.compose.channel;
|
|
469
|
+
if (vm.compose.subject) payload.subject = vm.compose.subject;
|
|
470
|
+
if (vm.replyTo) payload.replyTo = vm.replyTo.id;
|
|
471
|
+
if (vm.compose.claimable) payload.claimable = true;
|
|
472
|
+
return payload;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function doSend() {
|
|
476
|
+
if (!this.compose.from || !this.compose.to || !this.compose.body) {
|
|
477
|
+
alert("From, To, and Message are required.");
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
try {
|
|
482
|
+
await this.api("POST", "/messages", buildMessagePayload(this));
|
|
483
|
+
this.composeOpen = false;
|
|
484
|
+
this.replyTo = null;
|
|
485
|
+
this.compose = { ...DEFAULT_COMPOSE };
|
|
486
|
+
} catch (e) {
|
|
487
|
+
alert("Send failed: " + e.message);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function doClaim(msgId) {
|
|
492
|
+
if (!this.compose.from && !this.selectedAgent) {
|
|
493
|
+
alert('Select a "From" agent first (open Compose to pick one).');
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const agentId = this.compose.from || this.selectedAgent;
|
|
498
|
+
try {
|
|
499
|
+
const result = await this.api("POST", "/messages/" + msgId + "/claim", { agentId });
|
|
500
|
+
if (!result.ok) alert("Claim failed: " + (result.error || "unknown"));
|
|
501
|
+
} catch (e) {
|
|
502
|
+
alert("Claim failed: " + e.message);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function doDeleteMessage(id) {
|
|
507
|
+
try {
|
|
508
|
+
await this.api("DELETE", "/messages/" + id);
|
|
509
|
+
this.messages = this.messages.filter((msg) => msg.id !== id);
|
|
510
|
+
} catch (e) {
|
|
511
|
+
alert("Delete failed: " + e.message);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async function openThread(threadId) {
|
|
516
|
+
this.threadMessages = [];
|
|
517
|
+
this.threadOpen = true;
|
|
518
|
+
try {
|
|
519
|
+
this.threadMessages = await this.api("GET", "/messages/" + threadId + "/thread");
|
|
520
|
+
} catch (e) {
|
|
521
|
+
alert("Failed to load thread: " + e.message);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async function openTaskEvents(task) {
|
|
526
|
+
this.taskEvents = [];
|
|
527
|
+
this.taskEventsOpen = true;
|
|
528
|
+
try {
|
|
529
|
+
this.taskEvents = await this.api("GET", "/tasks/" + task.id + "/events");
|
|
530
|
+
} catch (e) {
|
|
531
|
+
alert("Failed to load task events: " + e.message);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function createAgentActions() {
|
|
536
|
+
return {
|
|
537
|
+
openRename(agent) {
|
|
538
|
+
this.renameModal = { show: true, agentId: agent.id, label: agent.label || "" };
|
|
539
|
+
this.$nextTick(() => this.$refs.renameInput?.focus());
|
|
540
|
+
},
|
|
541
|
+
|
|
542
|
+
async doRename() {
|
|
543
|
+
const label = this.renameModal.label.trim() || null;
|
|
544
|
+
try {
|
|
545
|
+
await this.api("PATCH", "/agents/" + this.renameModal.agentId + "/label", { label });
|
|
546
|
+
this.renameModal.show = false;
|
|
547
|
+
} catch (e) {
|
|
548
|
+
alert("Rename failed: " + e.message);
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
|
|
552
|
+
openConfirm(title, message, action) {
|
|
553
|
+
this.confirmModal = { show: true, title, message, action };
|
|
554
|
+
},
|
|
555
|
+
|
|
556
|
+
async doDeleteAgent(id) {
|
|
557
|
+
try {
|
|
558
|
+
await this.api("DELETE", "/agents/" + id);
|
|
559
|
+
if (this.selectedAgent === id) this.selectedAgent = "";
|
|
560
|
+
this.agents = this.agents.filter((agent) => agent.id !== id);
|
|
561
|
+
delete this.agentsById[id];
|
|
562
|
+
} catch (e) {
|
|
563
|
+
alert("Delete failed: " + e.message);
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function createChartMethods() {
|
|
570
|
+
return {
|
|
571
|
+
renderCharts,
|
|
572
|
+
renderVolumeChart,
|
|
573
|
+
renderStatusChart,
|
|
574
|
+
renderAgentChart,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function renderCharts() {
|
|
579
|
+
this.renderVolumeChart();
|
|
580
|
+
this.renderStatusChart();
|
|
581
|
+
this.renderAgentChart();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function renderVolumeChart() {
|
|
585
|
+
destroyChart(this, "volume");
|
|
586
|
+
|
|
587
|
+
const el = document.querySelector("#chart-volume");
|
|
588
|
+
if (!el) return;
|
|
589
|
+
|
|
590
|
+
this.chartInstances.volume = new ApexCharts(el, {
|
|
591
|
+
chart: { type: "area", height: 280, background: "transparent", toolbar: { show: false } },
|
|
592
|
+
theme: { mode: "dark" },
|
|
593
|
+
series: [{ name: "Messages", data: buildVolumeSeries(this.messages) }],
|
|
594
|
+
xaxis: { type: "datetime" },
|
|
595
|
+
stroke: { curve: "smooth", width: 2 },
|
|
596
|
+
fill: { type: "gradient", gradient: { opacityFrom: 0.4, opacityTo: 0 } },
|
|
597
|
+
dataLabels: { enabled: false },
|
|
598
|
+
colors: ["#4299e1"],
|
|
599
|
+
grid: { borderColor: "rgba(255,255,255,0.06)" },
|
|
600
|
+
});
|
|
601
|
+
this.chartInstances.volume.render();
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function buildVolumeSeries(messages) {
|
|
605
|
+
const buckets = {};
|
|
606
|
+
for (const msg of messages) {
|
|
607
|
+
const day = msg.createdAt ? new Date(msg.createdAt).toISOString().split("T")[0] : null;
|
|
608
|
+
if (day) buckets[day] = (buckets[day] || 0) + 1;
|
|
609
|
+
}
|
|
610
|
+
return Object.entries(buckets)
|
|
611
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
612
|
+
.map(([day, count]) => ({ x: day, y: count }));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function renderStatusChart() {
|
|
616
|
+
destroyChart(this, "status");
|
|
617
|
+
|
|
618
|
+
const el = document.querySelector("#chart-status");
|
|
619
|
+
if (!el) return;
|
|
620
|
+
|
|
621
|
+
const { labels, series } = countAgentStatuses(this.agents);
|
|
622
|
+
const colorMap = { online: "#48bb78", idle: "#48bb78", busy: "#ecc94b", offline: "#718096" };
|
|
623
|
+
|
|
624
|
+
this.chartInstances.status = new ApexCharts(el, {
|
|
625
|
+
chart: { type: "donut", height: 280, background: "transparent" },
|
|
626
|
+
theme: { mode: "dark" },
|
|
627
|
+
series,
|
|
628
|
+
labels,
|
|
629
|
+
colors: labels.map((label) => colorMap[label] || "#718096"),
|
|
630
|
+
legend: { position: "bottom" },
|
|
631
|
+
dataLabels: { enabled: true },
|
|
632
|
+
});
|
|
633
|
+
this.chartInstances.status.render();
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function countAgentStatuses(agents) {
|
|
637
|
+
const counts = { online: 0, idle: 0, busy: 0, offline: 0 };
|
|
638
|
+
for (const agent of agents) counts[agent.status] = (counts[agent.status] || 0) + 1;
|
|
639
|
+
|
|
640
|
+
const labels = Object.keys(counts).filter((key) => counts[key] > 0);
|
|
641
|
+
return { labels, series: labels.map((key) => counts[key]) };
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function renderAgentChart() {
|
|
645
|
+
destroyChart(this, "agents");
|
|
646
|
+
|
|
647
|
+
const el = document.querySelector("#chart-agents");
|
|
648
|
+
if (!el) return;
|
|
649
|
+
|
|
650
|
+
const sorted = countMessagesByAgent(this);
|
|
651
|
+
this.chartInstances.agents = new ApexCharts(el, {
|
|
652
|
+
chart: { type: "bar", height: 280, background: "transparent", toolbar: { show: false } },
|
|
653
|
+
theme: { mode: "dark" },
|
|
654
|
+
series: [{ name: "Messages", data: sorted.map(([, count]) => count) }],
|
|
655
|
+
xaxis: { categories: sorted.map(([name]) => name) },
|
|
656
|
+
colors: ["#4299e1"],
|
|
657
|
+
plotOptions: { bar: { borderRadius: 4, distributed: true } },
|
|
658
|
+
legend: { show: false },
|
|
659
|
+
grid: { borderColor: "rgba(255,255,255,0.06)" },
|
|
660
|
+
});
|
|
661
|
+
this.chartInstances.agents.render();
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function countMessagesByAgent(vm) {
|
|
665
|
+
const counts = {};
|
|
666
|
+
for (const msg of vm.messages) {
|
|
667
|
+
const name = vm.displayTarget(msg.from);
|
|
668
|
+
counts[name] = (counts[name] || 0) + 1;
|
|
669
|
+
}
|
|
670
|
+
return Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function destroyChart(vm, name) {
|
|
674
|
+
if (vm.chartInstances[name]) vm.chartInstances[name].destroy();
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function createRelayDashboard() {
|
|
678
|
+
const dashboard = {
|
|
679
|
+
...initialState(),
|
|
680
|
+
...createLifecycleMethods(),
|
|
681
|
+
...createSseMethods(),
|
|
682
|
+
...createApiMethods(),
|
|
683
|
+
...createDisplayMethods(),
|
|
684
|
+
...createMessageActions(),
|
|
685
|
+
...createAgentActions(),
|
|
686
|
+
...createChartMethods(),
|
|
687
|
+
};
|
|
688
|
+
Object.defineProperties(dashboard, createComputedDescriptors());
|
|
689
|
+
return dashboard;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
window.AgentRelayDashboard = {
|
|
693
|
+
createRelayDashboard,
|
|
694
|
+
helpers: { loadPref, savePref, indexAgents, upsertById, upsertTask, compareAgents },
|
|
695
|
+
};
|
|
696
|
+
window.relay = createRelayDashboard;
|
|
697
|
+
|
|
698
|
+
document.addEventListener("alpine:init", () => {
|
|
699
|
+
Alpine.data("relay", createRelayDashboard);
|
|
700
|
+
});
|
|
701
|
+
})();
|