agentgate 0.3.0 → 0.3.2
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/package.json +1 -1
- package/src/lib/queueExecutor.js +4 -4
- package/src/routes/bluesky.js +9 -0
- package/src/routes/ui.js +11 -160
- package/src/lib/notifier.js +0 -286
package/package.json
CHANGED
package/src/lib/queueExecutor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getAccountCredentials, setAccountCredentials, updateQueueStatus, getQueueEntry } from './db.js';
|
|
2
|
-
import {
|
|
2
|
+
import { notifyAgentQueueStatus } from './agentNotifier.js';
|
|
3
3
|
|
|
4
4
|
// Service base URLs
|
|
5
5
|
const SERVICE_URLS = {
|
|
@@ -277,8 +277,8 @@ function finalizeEntry(entryId, status, results) {
|
|
|
277
277
|
|
|
278
278
|
// Send notification to Clawdbot (async, don't block)
|
|
279
279
|
const updatedEntry = getQueueEntry(entryId);
|
|
280
|
-
|
|
281
|
-
console.error('[
|
|
280
|
+
notifyAgentQueueStatus(updatedEntry).catch(err => {
|
|
281
|
+
console.error('[agentNotifier] Failed to notify agent:', err.message);
|
|
282
282
|
});
|
|
283
283
|
}
|
|
284
284
|
|
|
@@ -327,7 +327,7 @@ export async function executeQueueEntry(entry) {
|
|
|
327
327
|
headers
|
|
328
328
|
};
|
|
329
329
|
|
|
330
|
-
if (req.body && ['POST', 'PUT', 'PATCH'].includes(req.method.toUpperCase())) {
|
|
330
|
+
if (req.body && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method.toUpperCase())) {
|
|
331
331
|
if (req.binaryBase64) {
|
|
332
332
|
// Binary data encoded as base64 (for blob uploads)
|
|
333
333
|
fetchOptions.body = Buffer.from(req.body, 'base64');
|
package/src/routes/bluesky.js
CHANGED
|
@@ -16,6 +16,15 @@ export const serviceInfo = {
|
|
|
16
16
|
'GET /api/bluesky/{accountName}/app.bsky.feed.getTimeline',
|
|
17
17
|
'GET /api/bluesky/{accountName}/app.bsky.feed.getAuthorFeed?actor={handle}',
|
|
18
18
|
'GET /api/bluesky/{accountName}/app.bsky.actor.getProfile?actor={handle}'
|
|
19
|
+
],
|
|
20
|
+
writeGuidelines: [
|
|
21
|
+
'Posts require FACETS for clickable links, mentions, and hashtags - they are NOT auto-detected',
|
|
22
|
+
'Facet positions use UTF-8 BYTE offsets, not character positions (emoji=4 bytes, em-dash=3 bytes)',
|
|
23
|
+
'Link facet: { index: { byteStart, byteEnd }, features: [{ $type: "app.bsky.richtext.facet#link", uri: "https://..." }] }',
|
|
24
|
+
'Mention facet requires DID (resolve via com.atproto.identity.resolveHandle), not handle',
|
|
25
|
+
'Hashtag facet: tag value should NOT include the # symbol',
|
|
26
|
+
'Always include "langs" array (e.g. ["en"]) and "createdAt" ISO timestamp',
|
|
27
|
+
'Use TextEncoder to calculate byte offsets: encoder.encode(text.slice(0, start)).length'
|
|
19
28
|
]
|
|
20
29
|
};
|
|
21
30
|
|
package/src/routes/ui.js
CHANGED
|
@@ -4,14 +4,12 @@ import {
|
|
|
4
4
|
setAdminPassword, verifyAdminPassword, hasAdminPassword,
|
|
5
5
|
listQueueEntries, getQueueEntry, updateQueueStatus, clearQueueByStatus, deleteQueueEntry, getPendingQueueCount, getQueueCounts,
|
|
6
6
|
listApiKeys, createApiKey, deleteApiKey, updateAgentWebhook, getApiKeyById,
|
|
7
|
-
listUnnotifiedEntries,
|
|
8
7
|
getMessagingMode, setMessagingMode, listPendingMessages, listAgentMessages,
|
|
9
8
|
approveAgentMessage, rejectAgentMessage, deleteAgentMessage, clearAgentMessagesByStatus, getMessageCounts, getAgentMessage
|
|
10
9
|
} from '../lib/db.js';
|
|
11
10
|
import { connectHsync, disconnectHsync, getHsyncUrl, isHsyncConnected } from '../lib/hsyncManager.js';
|
|
12
11
|
import { executeQueueEntry } from '../lib/queueExecutor.js';
|
|
13
|
-
import {
|
|
14
|
-
import { notifyAgentMessage, notifyMessageRejected } from '../lib/agentNotifier.js';
|
|
12
|
+
import { notifyAgentMessage, notifyMessageRejected, notifyAgentQueueStatus } from '../lib/agentNotifier.js';
|
|
15
13
|
import { registerAllRoutes, renderAllCards } from './ui/index.js';
|
|
16
14
|
|
|
17
15
|
const router = Router();
|
|
@@ -126,11 +124,10 @@ router.get('/', (req, res) => {
|
|
|
126
124
|
const hsyncUrl = getHsyncUrl();
|
|
127
125
|
const hsyncConnected = isHsyncConnected();
|
|
128
126
|
const pendingQueueCount = getPendingQueueCount();
|
|
129
|
-
const notificationsConfig = getSetting('notifications');
|
|
130
127
|
const messagingMode = getMessagingMode();
|
|
131
128
|
const pendingMessagesCount = listPendingMessages().length;
|
|
132
129
|
|
|
133
|
-
res.send(renderPage(accounts, { hsyncConfig, hsyncUrl, hsyncConnected, pendingQueueCount,
|
|
130
|
+
res.send(renderPage(accounts, { hsyncConfig, hsyncUrl, hsyncConnected, pendingQueueCount, messagingMode, pendingMessagesCount }));
|
|
134
131
|
});
|
|
135
132
|
|
|
136
133
|
// Register all service routes (github, bluesky, reddit, etc.)
|
|
@@ -157,78 +154,9 @@ router.post('/hsync/delete', async (req, res) => {
|
|
|
157
154
|
res.redirect('/ui');
|
|
158
155
|
});
|
|
159
156
|
|
|
160
|
-
// Notification settings
|
|
161
|
-
router.post('/notifications/setup', (req, res) => {
|
|
162
|
-
const { url, token, events } = req.body;
|
|
163
|
-
if (!url) {
|
|
164
|
-
return res.status(400).send('Webhook URL required');
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Parse events - could be comma-separated string or array
|
|
168
|
-
let eventList = ['completed', 'failed'];
|
|
169
|
-
if (events) {
|
|
170
|
-
eventList = Array.isArray(events) ? events : events.split(',').map(e => e.trim());
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
setSetting('notifications', {
|
|
174
|
-
clawdbot: {
|
|
175
|
-
enabled: true,
|
|
176
|
-
url: url.replace(/\/$/, ''),
|
|
177
|
-
token: token || '',
|
|
178
|
-
events: eventList,
|
|
179
|
-
retryAttempts: 3,
|
|
180
|
-
retryDelayMs: 5000
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
res.redirect('/ui');
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
router.post('/notifications/delete', (req, res) => {
|
|
187
|
-
deleteSetting('notifications');
|
|
188
|
-
res.redirect('/ui');
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
router.post('/notifications/test', async (req, res) => {
|
|
192
|
-
const wantsJson = req.headers.accept?.includes('application/json');
|
|
193
|
-
const config = getSetting('notifications');
|
|
157
|
+
// Notification settings removed - using agent-specific webhooks
|
|
194
158
|
|
|
195
|
-
if (!config?.clawdbot?.enabled || !config.clawdbot.url || !config.clawdbot.token) {
|
|
196
|
-
const error = 'Notifications not configured';
|
|
197
|
-
return wantsJson
|
|
198
|
-
? res.status(400).json({ success: false, error })
|
|
199
|
-
: res.status(400).send(error);
|
|
200
|
-
}
|
|
201
159
|
|
|
202
|
-
try {
|
|
203
|
-
const response = await fetch(config.clawdbot.url, {
|
|
204
|
-
method: 'POST',
|
|
205
|
-
headers: {
|
|
206
|
-
'Authorization': `Bearer ${config.clawdbot.token}`,
|
|
207
|
-
'Content-Type': 'application/json'
|
|
208
|
-
},
|
|
209
|
-
body: JSON.stringify({
|
|
210
|
-
text: '🧪 [agentgate] Test notification - webhook is working!',
|
|
211
|
-
mode: 'now'
|
|
212
|
-
})
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
if (response.ok) {
|
|
216
|
-
return wantsJson
|
|
217
|
-
? res.json({ success: true })
|
|
218
|
-
: res.redirect('/ui?notification_test=success');
|
|
219
|
-
} else {
|
|
220
|
-
const text = await response.text().catch(() => '');
|
|
221
|
-
const error = `HTTP ${response.status}: ${text.substring(0, 100)}`;
|
|
222
|
-
return wantsJson
|
|
223
|
-
? res.status(400).json({ success: false, error })
|
|
224
|
-
: res.redirect('/ui?notification_test=failed');
|
|
225
|
-
}
|
|
226
|
-
} catch (err) {
|
|
227
|
-
return wantsJson
|
|
228
|
-
? res.status(500).json({ success: false, error: err.message })
|
|
229
|
-
: res.redirect('/ui?notification_test=failed');
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
160
|
|
|
233
161
|
// Agent Messaging settings
|
|
234
162
|
router.post('/messaging/mode', (req, res) => {
|
|
@@ -354,8 +282,7 @@ router.get('/queue', (req, res) => {
|
|
|
354
282
|
entries = listQueueEntries(filter);
|
|
355
283
|
}
|
|
356
284
|
const counts = getQueueCounts();
|
|
357
|
-
|
|
358
|
-
res.send(renderQueuePage(entries, filter, counts, unnotified.length));
|
|
285
|
+
res.send(renderQueuePage(entries, filter, counts));
|
|
359
286
|
});
|
|
360
287
|
|
|
361
288
|
router.post('/queue/:id/approve', async (req, res) => {
|
|
@@ -412,10 +339,10 @@ router.post('/queue/:id/reject', async (req, res) => {
|
|
|
412
339
|
|
|
413
340
|
updateQueueStatus(id, 'rejected', { rejection_reason: reason || 'No reason provided' });
|
|
414
341
|
|
|
415
|
-
// Send notification to Clawdbot
|
|
342
|
+
// Send notification to submitting agent and global Clawdbot webhook
|
|
416
343
|
const updated = getQueueEntry(id);
|
|
417
|
-
|
|
418
|
-
console.error('[
|
|
344
|
+
notifyAgentQueueStatus(updated).catch(err => {
|
|
345
|
+
console.error('[agentNotifier] Failed to notify agent:', err.message);
|
|
419
346
|
});
|
|
420
347
|
|
|
421
348
|
const counts = getQueueCounts();
|
|
@@ -472,35 +399,15 @@ router.post('/queue/:id/notify', async (req, res) => {
|
|
|
472
399
|
const { id } = req.params;
|
|
473
400
|
const wantsJson = req.headers.accept?.includes('application/json');
|
|
474
401
|
|
|
475
|
-
const result = await retryNotification(id, getQueueEntry);
|
|
476
402
|
const updated = getQueueEntry(id);
|
|
477
403
|
|
|
478
404
|
if (wantsJson) {
|
|
479
|
-
return res.json({ success:
|
|
405
|
+
return res.json({ success: true, entry: updated });
|
|
480
406
|
}
|
|
481
407
|
res.redirect('/ui/queue');
|
|
482
408
|
});
|
|
483
409
|
|
|
484
|
-
// Retry all failed notifications
|
|
485
|
-
router.post('/queue/notify-all', async (req, res) => {
|
|
486
|
-
const wantsJson = req.headers.accept?.includes('application/json');
|
|
487
|
-
const unnotified = listUnnotifiedEntries();
|
|
488
410
|
|
|
489
|
-
if (unnotified.length === 0) {
|
|
490
|
-
return wantsJson
|
|
491
|
-
? res.json({ success: true, count: 0 })
|
|
492
|
-
: res.redirect('/ui/queue');
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// Batch into single notification
|
|
496
|
-
const { notifyClawdbotBatch } = await import('../lib/notifier.js');
|
|
497
|
-
const result = await notifyClawdbotBatch(unnotified);
|
|
498
|
-
|
|
499
|
-
if (wantsJson) {
|
|
500
|
-
return res.json({ success: result.success, error: result.error, count: unnotified.length });
|
|
501
|
-
}
|
|
502
|
-
res.redirect('/ui/queue');
|
|
503
|
-
});
|
|
504
411
|
|
|
505
412
|
// API Keys Management
|
|
506
413
|
router.get('/keys', (req, res) => {
|
|
@@ -577,8 +484,7 @@ router.delete('/keys/:id', (req, res) => {
|
|
|
577
484
|
|
|
578
485
|
// HTML Templates
|
|
579
486
|
|
|
580
|
-
function renderPage(accounts, { hsyncConfig, hsyncUrl, hsyncConnected, pendingQueueCount,
|
|
581
|
-
const clawdbotConfig = notificationsConfig?.clawdbot;
|
|
487
|
+
function renderPage(accounts, { hsyncConfig, hsyncUrl, hsyncConnected, pendingQueueCount, messagingMode, pendingMessagesCount }) {
|
|
582
488
|
return `<!DOCTYPE html>
|
|
583
489
|
<html>
|
|
584
490
|
<head>
|
|
@@ -695,61 +601,6 @@ curl -X POST http://localhost:${PORT}/api/queue/github/personal/submit \\
|
|
|
695
601
|
</div>
|
|
696
602
|
|
|
697
603
|
<div class="card">
|
|
698
|
-
<details ${clawdbotConfig?.enabled ? 'open' : ''}>
|
|
699
|
-
<summary>Clawdbot Notifications ${clawdbotConfig?.enabled ? '<span class="status configured">Configured</span>' : ''}</summary>
|
|
700
|
-
<div style="margin-top: 16px;">
|
|
701
|
-
<div id="notification-feedback"></div>
|
|
702
|
-
${clawdbotConfig?.enabled ? `
|
|
703
|
-
<p>Webhook URL: <strong>${clawdbotConfig.url}</strong></p>
|
|
704
|
-
<p>Events: <code>${(clawdbotConfig.events || ['completed', 'failed']).join(', ')}</code></p>
|
|
705
|
-
<div style="display: flex; gap: 8px; margin-top: 12px;">
|
|
706
|
-
<button type="button" class="btn-primary btn-sm" id="test-notification-btn" onclick="testNotification()">Send Test</button>
|
|
707
|
-
<form method="POST" action="/ui/notifications/delete" style="margin: 0;">
|
|
708
|
-
<button type="submit" class="btn-danger btn-sm">Disable</button>
|
|
709
|
-
</form>
|
|
710
|
-
</div>
|
|
711
|
-
<script>
|
|
712
|
-
async function testNotification() {
|
|
713
|
-
const btn = document.getElementById('test-notification-btn');
|
|
714
|
-
const feedback = document.getElementById('notification-feedback');
|
|
715
|
-
btn.disabled = true;
|
|
716
|
-
btn.textContent = 'Sending...';
|
|
717
|
-
feedback.innerHTML = '';
|
|
718
|
-
|
|
719
|
-
try {
|
|
720
|
-
const res = await fetch('/ui/notifications/test', {
|
|
721
|
-
method: 'POST',
|
|
722
|
-
headers: { 'Accept': 'application/json' }
|
|
723
|
-
});
|
|
724
|
-
const data = await res.json();
|
|
725
|
-
|
|
726
|
-
if (data.success) {
|
|
727
|
-
feedback.innerHTML = '<div class="success-message" style="margin-bottom: 16px;">✓ Test notification sent!</div>';
|
|
728
|
-
} else {
|
|
729
|
-
feedback.innerHTML = '<div class="error-message" style="margin-bottom: 16px;">✗ ' + (data.error || 'Failed to send') + '</div>';
|
|
730
|
-
}
|
|
731
|
-
} catch (err) {
|
|
732
|
-
feedback.innerHTML = '<div class="error-message" style="margin-bottom: 16px;">✗ ' + err.message + '</div>';
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
btn.disabled = false;
|
|
736
|
-
btn.textContent = 'Send Test';
|
|
737
|
-
}
|
|
738
|
-
</script>
|
|
739
|
-
` : `
|
|
740
|
-
<p class="help">Send notifications to <a href="https://docs.clawd.bot" target="_blank">Clawdbot</a> when queue items are completed, failed, or rejected.</p>
|
|
741
|
-
<form method="POST" action="/ui/notifications/setup">
|
|
742
|
-
<label>Webhook URL</label>
|
|
743
|
-
<input type="text" name="url" placeholder="https://your-gateway.example.com/hooks/wake" required>
|
|
744
|
-
<label>Token</label>
|
|
745
|
-
<input type="password" name="token" placeholder="Clawdbot hooks token" required>
|
|
746
|
-
<label>Events (comma-separated)</label>
|
|
747
|
-
<input type="text" name="events" placeholder="completed, failed, rejected" value="completed, failed, rejected">
|
|
748
|
-
<button type="submit" class="btn-primary">Enable Notifications</button>
|
|
749
|
-
</form>
|
|
750
|
-
`}
|
|
751
|
-
</div>
|
|
752
|
-
</details>
|
|
753
604
|
</div>
|
|
754
605
|
</body>
|
|
755
606
|
</html>`;
|
|
@@ -804,7 +655,7 @@ function renderSetupPasswordPage(error = '') {
|
|
|
804
655
|
</html>`;
|
|
805
656
|
}
|
|
806
657
|
|
|
807
|
-
function renderQueuePage(entries, filter, counts
|
|
658
|
+
function renderQueuePage(entries, filter, counts = 0) {
|
|
808
659
|
const escapeHtml = (str) => {
|
|
809
660
|
if (typeof str !== 'string') str = JSON.stringify(str, null, 2);
|
|
810
661
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
@@ -1126,7 +977,7 @@ function renderQueuePage(entries, filter, counts, unnotifiedCount = 0) {
|
|
|
1126
977
|
<div class="filter-bar" id="filter-bar">
|
|
1127
978
|
${filterLinks}
|
|
1128
979
|
<div class="clear-section">
|
|
1129
|
-
|
|
980
|
+
|
|
1130
981
|
${filter === 'completed' && counts.completed > 0 ? '<button type="button" class="btn-sm btn-danger" onclick="clearByStatus(\'completed\')">Clear Completed</button>' : ''}
|
|
1131
982
|
${filter === 'failed' && counts.failed > 0 ? '<button type="button" class="btn-sm btn-danger" onclick="clearByStatus(\'failed\')">Clear Failed</button>' : ''}
|
|
1132
983
|
${filter === 'rejected' && counts.rejected > 0 ? '<button type="button" class="btn-sm btn-danger" onclick="clearByStatus(\'rejected\')">Clear Rejected</button>' : ''}
|
package/src/lib/notifier.js
DELETED
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
import { getSetting, updateQueueNotification } from './db.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Send a notification to Clawdbot webhook when queue items complete/fail
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const DEFAULT_RETRY_ATTEMPTS = 3;
|
|
8
|
-
const DEFAULT_RETRY_DELAY_MS = 5000;
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Find the most relevant result to display (usually last one with a URL, or first failure)
|
|
12
|
-
*/
|
|
13
|
-
function findRelevantResult(results, forError = false) {
|
|
14
|
-
if (!results?.length) return null;
|
|
15
|
-
|
|
16
|
-
if (forError) {
|
|
17
|
-
// For errors, find the first non-ok result
|
|
18
|
-
return results.find(r => !r.ok) || results[results.length - 1];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// For success, find the last result with a useful URL (PR, issue, etc.)
|
|
22
|
-
// Search backwards to get the most relevant one (e.g., PR URL, not branch ref)
|
|
23
|
-
for (let i = results.length - 1; i >= 0; i--) {
|
|
24
|
-
const r = results[i];
|
|
25
|
-
if (r.body?.html_url || r.body?.url) {
|
|
26
|
-
return r;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Fallback to last result
|
|
31
|
-
return results[results.length - 1];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Format the notification text for a queue entry
|
|
36
|
-
*/
|
|
37
|
-
function formatNotification(entry) {
|
|
38
|
-
const emoji = entry.status === 'completed' ? '✅' :
|
|
39
|
-
entry.status === 'failed' ? '❌' : '🚫';
|
|
40
|
-
|
|
41
|
-
let text = `${emoji} [agentgate] Queue #${entry.id} ${entry.status}`;
|
|
42
|
-
text += `\n→ ${entry.service}/${entry.account_name}`;
|
|
43
|
-
|
|
44
|
-
// Include key result info (e.g., PR URL, issue URL)
|
|
45
|
-
if (entry.status === 'completed' && entry.results?.length) {
|
|
46
|
-
const relevantResult = findRelevantResult(entry.results, false);
|
|
47
|
-
if (relevantResult?.body) {
|
|
48
|
-
// GitHub PR/Issue
|
|
49
|
-
if (relevantResult.body.html_url) {
|
|
50
|
-
text += `\n→ ${relevantResult.body.html_url}`;
|
|
51
|
-
}
|
|
52
|
-
// Other useful fields
|
|
53
|
-
else if (relevantResult.body.url) {
|
|
54
|
-
text += `\n→ ${relevantResult.body.url}`;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Include error info for failures
|
|
60
|
-
if (entry.status === 'failed' && entry.results?.length) {
|
|
61
|
-
const failingResult = findRelevantResult(entry.results, true);
|
|
62
|
-
if (failingResult.error) {
|
|
63
|
-
text += `\n→ Error: ${failingResult.error}`;
|
|
64
|
-
} else if (failingResult.body?.message) {
|
|
65
|
-
text += `\n→ Error: ${failingResult.body.message}`;
|
|
66
|
-
} else if (!failingResult.ok && failingResult.status) {
|
|
67
|
-
text += `\n→ Error: HTTP ${failingResult.status}`;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Include rejection reason
|
|
72
|
-
if (entry.status === 'rejected' && entry.rejection_reason) {
|
|
73
|
-
text += `\n→ Reason: ${entry.rejection_reason}`;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Original comment/intent
|
|
77
|
-
if (entry.comment) {
|
|
78
|
-
// Truncate long comments
|
|
79
|
-
const comment = entry.comment.length > 100
|
|
80
|
-
? entry.comment.substring(0, 100) + '...'
|
|
81
|
-
: entry.comment;
|
|
82
|
-
text += `\nOriginal: "${comment}"`;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return text;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Sleep helper
|
|
90
|
-
*/
|
|
91
|
-
function sleep(ms) {
|
|
92
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Send notification to Clawdbot
|
|
97
|
-
* @param {object} entry - The queue entry to notify about
|
|
98
|
-
* @returns {Promise<{success: boolean, error?: string}>}
|
|
99
|
-
*/
|
|
100
|
-
export async function notifyClawdbot(entry) {
|
|
101
|
-
const config = getSetting('notifications');
|
|
102
|
-
|
|
103
|
-
if (!config?.clawdbot?.enabled) {
|
|
104
|
-
return { success: true, skipped: true };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const { url, token, events, retryAttempts, retryDelayMs } = config.clawdbot;
|
|
108
|
-
|
|
109
|
-
// Check if this event type should be notified
|
|
110
|
-
const allowedEvents = events || ['completed', 'failed'];
|
|
111
|
-
if (!allowedEvents.includes(entry.status)) {
|
|
112
|
-
return { success: true, skipped: true };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (!url || !token) {
|
|
116
|
-
const error = 'Clawdbot notification config incomplete (missing url or token)';
|
|
117
|
-
updateQueueNotification(entry.id, false, error);
|
|
118
|
-
return { success: false, error };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const payload = {
|
|
122
|
-
text: formatNotification(entry),
|
|
123
|
-
mode: 'now'
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
const maxAttempts = retryAttempts || DEFAULT_RETRY_ATTEMPTS;
|
|
127
|
-
const delay = retryDelayMs || DEFAULT_RETRY_DELAY_MS;
|
|
128
|
-
let lastError = null;
|
|
129
|
-
|
|
130
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
131
|
-
try {
|
|
132
|
-
const response = await fetch(url, {
|
|
133
|
-
method: 'POST',
|
|
134
|
-
headers: {
|
|
135
|
-
'Authorization': `Bearer ${token}`,
|
|
136
|
-
'Content-Type': 'application/json'
|
|
137
|
-
},
|
|
138
|
-
body: JSON.stringify(payload)
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
if (response.ok) {
|
|
142
|
-
// Success
|
|
143
|
-
updateQueueNotification(entry.id, true, null);
|
|
144
|
-
return { success: true };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Non-OK response
|
|
148
|
-
const text = await response.text().catch(() => '');
|
|
149
|
-
lastError = `HTTP ${response.status}: ${text.substring(0, 100)}`;
|
|
150
|
-
|
|
151
|
-
} catch (err) {
|
|
152
|
-
lastError = err.message;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Retry after delay (unless last attempt)
|
|
156
|
-
if (attempt < maxAttempts) {
|
|
157
|
-
await sleep(delay);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// All retries failed
|
|
162
|
-
updateQueueNotification(entry.id, false, lastError);
|
|
163
|
-
return { success: false, error: lastError };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Retry notification for a specific queue entry
|
|
168
|
-
*/
|
|
169
|
-
export async function retryNotification(entryId, getQueueEntry) {
|
|
170
|
-
const entry = getQueueEntry(entryId);
|
|
171
|
-
if (!entry) {
|
|
172
|
-
return { success: false, error: 'Queue entry not found' };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Only retry for completed/failed/rejected entries
|
|
176
|
-
if (!['completed', 'failed', 'rejected'].includes(entry.status)) {
|
|
177
|
-
return { success: false, error: 'Can only notify for completed/failed/rejected entries' };
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return notifyClawdbot(entry);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Format a short summary line for batch notifications
|
|
185
|
-
*/
|
|
186
|
-
function formatBatchLine(entry) {
|
|
187
|
-
const emoji = entry.status === 'completed' ? '✅' :
|
|
188
|
-
entry.status === 'failed' ? '❌' : '🚫';
|
|
189
|
-
|
|
190
|
-
let line = `${emoji} #${entry.id.substring(0, 8)} - ${entry.service}/${entry.account_name}`;
|
|
191
|
-
|
|
192
|
-
// Add brief result info
|
|
193
|
-
if (entry.status === 'completed') {
|
|
194
|
-
const relevantResult = findRelevantResult(entry.results, false);
|
|
195
|
-
if (relevantResult?.body?.html_url) {
|
|
196
|
-
line += ` - ${relevantResult.body.html_url}`;
|
|
197
|
-
}
|
|
198
|
-
} else if (entry.status === 'failed') {
|
|
199
|
-
const failingResult = findRelevantResult(entry.results, true);
|
|
200
|
-
const err = failingResult?.error || failingResult?.body?.message || (failingResult && !failingResult.ok ? `HTTP ${failingResult.status || '?'}` : 'Unknown error');
|
|
201
|
-
line += ` - ${err.substring(0, 50)}`;
|
|
202
|
-
} else if (entry.status === 'rejected') {
|
|
203
|
-
line += ` - ${(entry.rejection_reason || 'rejected').substring(0, 50)}`;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return line;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Send a single batched notification for multiple queue entries
|
|
211
|
-
* @param {object[]} entries - Array of queue entries to notify about
|
|
212
|
-
* @returns {Promise<{success: boolean, error?: string}>}
|
|
213
|
-
*/
|
|
214
|
-
export async function notifyClawdbotBatch(entries) {
|
|
215
|
-
const config = getSetting('notifications');
|
|
216
|
-
|
|
217
|
-
if (!config?.clawdbot?.enabled) {
|
|
218
|
-
return { success: true, skipped: true };
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const { url, token, retryAttempts, retryDelayMs } = config.clawdbot;
|
|
222
|
-
|
|
223
|
-
if (!url || !token) {
|
|
224
|
-
const error = 'Clawdbot notification config incomplete (missing url or token)';
|
|
225
|
-
for (const entry of entries) {
|
|
226
|
-
updateQueueNotification(entry.id, false, error);
|
|
227
|
-
}
|
|
228
|
-
return { success: false, error };
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Build batched message
|
|
232
|
-
const completed = entries.filter(e => e.status === 'completed').length;
|
|
233
|
-
const failed = entries.filter(e => e.status === 'failed').length;
|
|
234
|
-
const rejected = entries.filter(e => e.status === 'rejected').length;
|
|
235
|
-
|
|
236
|
-
const counts = [];
|
|
237
|
-
if (completed) counts.push(`${completed} completed`);
|
|
238
|
-
if (failed) counts.push(`${failed} failed`);
|
|
239
|
-
if (rejected) counts.push(`${rejected} rejected`);
|
|
240
|
-
|
|
241
|
-
let text = `📬 [agentgate] Catch-up: ${entries.length} item${entries.length > 1 ? 's' : ''} (${counts.join(', ')})\n\n`;
|
|
242
|
-
text += entries.map(formatBatchLine).join('\n');
|
|
243
|
-
|
|
244
|
-
const payload = { text, mode: 'now' };
|
|
245
|
-
|
|
246
|
-
const maxAttempts = retryAttempts || DEFAULT_RETRY_ATTEMPTS;
|
|
247
|
-
const delay = retryDelayMs || DEFAULT_RETRY_DELAY_MS;
|
|
248
|
-
let lastError = null;
|
|
249
|
-
|
|
250
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
251
|
-
try {
|
|
252
|
-
const response = await fetch(url, {
|
|
253
|
-
method: 'POST',
|
|
254
|
-
headers: {
|
|
255
|
-
'Authorization': `Bearer ${token}`,
|
|
256
|
-
'Content-Type': 'application/json'
|
|
257
|
-
},
|
|
258
|
-
body: JSON.stringify(payload)
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
if (response.ok) {
|
|
262
|
-
// Success - mark all as notified
|
|
263
|
-
for (const entry of entries) {
|
|
264
|
-
updateQueueNotification(entry.id, true, null);
|
|
265
|
-
}
|
|
266
|
-
return { success: true };
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const respText = await response.text().catch(() => '');
|
|
270
|
-
lastError = `HTTP ${response.status}: ${respText.substring(0, 100)}`;
|
|
271
|
-
|
|
272
|
-
} catch (err) {
|
|
273
|
-
lastError = err.message;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (attempt < maxAttempts) {
|
|
277
|
-
await sleep(delay);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// All retries failed
|
|
282
|
-
for (const entry of entries) {
|
|
283
|
-
updateQueueNotification(entry.id, false, lastError);
|
|
284
|
-
}
|
|
285
|
-
return { success: false, error: lastError };
|
|
286
|
-
}
|