@chrysb/alphaclaw 0.8.0 → 0.8.1-beta.1
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/lib/public/js/app.js +100 -83
- package/lib/public/js/components/agents-tab/agent-pairing-section.js +47 -12
- package/lib/public/js/components/channels.js +14 -17
- package/lib/public/js/components/envars.js +42 -6
- package/lib/public/js/components/features.js +6 -12
- package/lib/public/js/components/general/use-general-tab.js +10 -5
- package/lib/public/js/components/google/use-gmail-watch.js +22 -18
- package/lib/public/js/components/google/use-google-accounts.js +23 -23
- package/lib/public/js/components/models-tab/use-models.js +20 -4
- package/lib/public/js/components/nodes-tab/connected-nodes/user-connected-nodes.js +2 -2
- package/lib/public/js/components/nodes-tab/use-nodes-tab.js +13 -9
- package/lib/public/js/components/routes/webhooks-route.js +1 -1
- package/lib/public/js/components/webhooks/create-webhook-modal/index.js +176 -0
- package/lib/public/js/components/webhooks/helpers.js +106 -0
- package/lib/public/js/components/webhooks/index.js +148 -0
- package/lib/public/js/components/webhooks/request-history/index.js +241 -0
- package/lib/public/js/components/webhooks/request-history/use-request-history.js +167 -0
- package/lib/public/js/components/webhooks/webhook-detail/index.js +374 -0
- package/lib/public/js/components/webhooks/webhook-detail/use-webhook-detail.js +261 -0
- package/lib/public/js/components/webhooks/webhook-list/index.js +96 -0
- package/lib/public/js/components/webhooks/webhook-list/use-webhook-list.js +30 -0
- package/lib/public/js/hooks/use-app-shell-controller.js +59 -6
- package/lib/public/js/hooks/use-cached-fetch.js +63 -0
- package/lib/public/js/hooks/usePolling.js +45 -7
- package/lib/public/js/lib/api-cache.js +88 -0
- package/lib/public/js/lib/api.js +64 -1
- package/lib/server/db/webhooks/index.js +144 -0
- package/lib/server/db/webhooks/schema.js +13 -0
- package/lib/server/init/register-server-routes.js +21 -0
- package/lib/server/oauth-callback-middleware.js +34 -0
- package/lib/server/routes/proxy.js +2 -0
- package/lib/server/routes/system.js +50 -2
- package/lib/server/routes/webhooks.js +126 -18
- package/lib/server/webhook-middleware.js +6 -1
- package/lib/server.js +12 -0
- package/package.json +1 -1
- package/lib/public/js/components/webhooks.js +0 -1259
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useMemo } from "https://esm.sh/preact/hooks";
|
|
3
|
+
import htm from "https://esm.sh/htm";
|
|
4
|
+
import { sendAgentMessage } from "../../../lib/api.js";
|
|
5
|
+
import { ActionButton } from "../../action-button.js";
|
|
6
|
+
import { AgentSendModal } from "../../agent-send-modal.js";
|
|
7
|
+
import { showToast } from "../../toast.js";
|
|
8
|
+
import {
|
|
9
|
+
buildWebhookDebugMessage,
|
|
10
|
+
formatBytes,
|
|
11
|
+
formatLastReceived,
|
|
12
|
+
getRequestStatusTone,
|
|
13
|
+
jsonPretty,
|
|
14
|
+
kStatusFilters,
|
|
15
|
+
} from "../helpers.js";
|
|
16
|
+
import { useRequestHistory } from "./use-request-history.js";
|
|
17
|
+
|
|
18
|
+
const html = htm.bind(h);
|
|
19
|
+
|
|
20
|
+
export const RequestHistory = ({
|
|
21
|
+
selectedHookName = "",
|
|
22
|
+
effectiveAuthMode = "headers",
|
|
23
|
+
webhookUrl = "",
|
|
24
|
+
webhookUrlWithQueryToken = "",
|
|
25
|
+
bearerTokenValue = "",
|
|
26
|
+
selectedWebhook = null,
|
|
27
|
+
}) => {
|
|
28
|
+
const { state, actions } = useRequestHistory({
|
|
29
|
+
selectedHookName,
|
|
30
|
+
effectiveAuthMode,
|
|
31
|
+
webhookUrl,
|
|
32
|
+
webhookUrlWithQueryToken,
|
|
33
|
+
bearerTokenValue,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const {
|
|
37
|
+
requests,
|
|
38
|
+
statusFilter,
|
|
39
|
+
expandedRows,
|
|
40
|
+
replayingRequestId,
|
|
41
|
+
debugLoadingRequestId,
|
|
42
|
+
debugRequest,
|
|
43
|
+
} = state;
|
|
44
|
+
|
|
45
|
+
const debugAgentMessage = useMemo(
|
|
46
|
+
() =>
|
|
47
|
+
buildWebhookDebugMessage({
|
|
48
|
+
hookName: selectedHookName,
|
|
49
|
+
webhook: selectedWebhook,
|
|
50
|
+
request: debugRequest,
|
|
51
|
+
}),
|
|
52
|
+
[debugRequest, selectedHookName, selectedWebhook],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return html`
|
|
56
|
+
<div class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
|
57
|
+
<div class="flex items-center justify-between gap-3">
|
|
58
|
+
<h3 class="card-label">Request history</h3>
|
|
59
|
+
<div class="flex items-center gap-2">
|
|
60
|
+
${kStatusFilters.map(
|
|
61
|
+
(filter) => html`
|
|
62
|
+
<button
|
|
63
|
+
class="text-xs px-2 py-1 rounded border ${statusFilter === filter
|
|
64
|
+
? "border-cyan-400 text-cyan-200 bg-cyan-400/10"
|
|
65
|
+
: "border-border text-gray-400 hover:text-gray-200"}"
|
|
66
|
+
onclick=${() => actions.handleSetStatusFilter(filter)}
|
|
67
|
+
>
|
|
68
|
+
${filter}
|
|
69
|
+
</button>
|
|
70
|
+
`,
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
${requests.length === 0
|
|
76
|
+
? html`<p class="text-sm text-gray-500">No requests logged yet.</p>`
|
|
77
|
+
: html`
|
|
78
|
+
<div class="ac-history-list">
|
|
79
|
+
${requests.map((item) => {
|
|
80
|
+
const statusTone = getRequestStatusTone(item.status);
|
|
81
|
+
return html`
|
|
82
|
+
<details
|
|
83
|
+
class="ac-history-item"
|
|
84
|
+
open=${expandedRows.has(item.id)}
|
|
85
|
+
ontoggle=${(e) =>
|
|
86
|
+
actions.handleRequestRowToggle(item.id, !!e.currentTarget?.open)}
|
|
87
|
+
>
|
|
88
|
+
<summary class="ac-history-summary">
|
|
89
|
+
<div class="ac-history-summary-row">
|
|
90
|
+
<span class="inline-flex items-center gap-2 min-w-0">
|
|
91
|
+
<span class="ac-history-toggle shrink-0" aria-hidden="true"
|
|
92
|
+
>▸</span
|
|
93
|
+
>
|
|
94
|
+
<span class="truncate text-xs text-gray-300">
|
|
95
|
+
${formatLastReceived(item.createdAt)}
|
|
96
|
+
</span>
|
|
97
|
+
</span>
|
|
98
|
+
<span class="inline-flex items-center gap-2 shrink-0">
|
|
99
|
+
<span class="text-xs text-gray-500"
|
|
100
|
+
>${formatBytes(item.payloadSize)}</span
|
|
101
|
+
>
|
|
102
|
+
<span class=${`text-xs font-medium ${statusTone.textClass}`}
|
|
103
|
+
>${item.gatewayStatus || "n/a"}</span
|
|
104
|
+
>
|
|
105
|
+
<span class="inline-flex items-center">
|
|
106
|
+
<span
|
|
107
|
+
class=${`h-2.5 w-2.5 rounded-full ${statusTone.dotClass}`}
|
|
108
|
+
title=${item.status || "unknown"}
|
|
109
|
+
aria-label=${item.status || "unknown"}
|
|
110
|
+
></span>
|
|
111
|
+
</span>
|
|
112
|
+
</span>
|
|
113
|
+
</div>
|
|
114
|
+
</summary>
|
|
115
|
+
${expandedRows.has(item.id)
|
|
116
|
+
? html`
|
|
117
|
+
<div class="ac-history-body space-y-3">
|
|
118
|
+
<div>
|
|
119
|
+
<p class="text-[11px] text-gray-500 mb-1">Headers</p>
|
|
120
|
+
<pre
|
|
121
|
+
class="text-xs bg-black/30 border border-border rounded p-2 overflow-auto"
|
|
122
|
+
>
|
|
123
|
+
${jsonPretty(item.headers)}</pre
|
|
124
|
+
>
|
|
125
|
+
<div class="mt-2 flex justify-start gap-2">
|
|
126
|
+
<button
|
|
127
|
+
class="h-7 text-xs px-2.5 rounded-lg ac-btn-secondary"
|
|
128
|
+
onclick=${() =>
|
|
129
|
+
actions.handleCopyRequestField(
|
|
130
|
+
jsonPretty(item.headers),
|
|
131
|
+
"Headers",
|
|
132
|
+
)}
|
|
133
|
+
>
|
|
134
|
+
Copy
|
|
135
|
+
</button>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
<div>
|
|
139
|
+
<p class="text-[11px] text-gray-500 mb-1">
|
|
140
|
+
Payload ${item.payloadTruncated ? "(truncated)" : ""}
|
|
141
|
+
</p>
|
|
142
|
+
<pre
|
|
143
|
+
class="text-xs bg-black/30 border border-border rounded p-2 overflow-auto"
|
|
144
|
+
>
|
|
145
|
+
${jsonPretty(item.payload)}</pre
|
|
146
|
+
>
|
|
147
|
+
<div class="mt-2 flex justify-start gap-2">
|
|
148
|
+
<button
|
|
149
|
+
class="h-7 text-xs px-2.5 rounded-lg ac-btn-secondary"
|
|
150
|
+
onclick=${() =>
|
|
151
|
+
actions.handleCopyRequestField(
|
|
152
|
+
item.payload,
|
|
153
|
+
"Payload",
|
|
154
|
+
)}
|
|
155
|
+
>
|
|
156
|
+
Copy
|
|
157
|
+
</button>
|
|
158
|
+
<button
|
|
159
|
+
class="h-7 text-xs px-2.5 rounded-lg ac-btn-secondary disabled:opacity-60"
|
|
160
|
+
onclick=${() => actions.handleReplayRequest(item)}
|
|
161
|
+
disabled=${item.payloadTruncated ||
|
|
162
|
+
replayingRequestId === item.id}
|
|
163
|
+
title=${item.payloadTruncated
|
|
164
|
+
? "Cannot replay truncated payload"
|
|
165
|
+
: "Replay this payload"}
|
|
166
|
+
>
|
|
167
|
+
${replayingRequestId === item.id
|
|
168
|
+
? "Replaying..."
|
|
169
|
+
: "Replay"}
|
|
170
|
+
</button>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
<div>
|
|
174
|
+
<p class="text-[11px] text-gray-500 mb-1">
|
|
175
|
+
Gateway response (${item.gatewayStatus || "n/a"})
|
|
176
|
+
</p>
|
|
177
|
+
<pre
|
|
178
|
+
class="text-xs bg-black/30 border border-border rounded p-2 overflow-auto"
|
|
179
|
+
>
|
|
180
|
+
${jsonPretty(item.gatewayBody)}</pre
|
|
181
|
+
>
|
|
182
|
+
<div class="mt-2 flex justify-start gap-2">
|
|
183
|
+
<button
|
|
184
|
+
class="h-7 text-xs px-2.5 rounded-lg ac-btn-secondary"
|
|
185
|
+
onclick=${() =>
|
|
186
|
+
actions.handleCopyRequestField(
|
|
187
|
+
item.gatewayBody,
|
|
188
|
+
"Gateway response",
|
|
189
|
+
)}
|
|
190
|
+
>
|
|
191
|
+
Copy
|
|
192
|
+
</button>
|
|
193
|
+
${item.status === "error"
|
|
194
|
+
? html`<${ActionButton}
|
|
195
|
+
onClick=${() =>
|
|
196
|
+
actions.handleAskAgentToDebug(item)}
|
|
197
|
+
loading=${debugLoadingRequestId === item.id}
|
|
198
|
+
tone="primary"
|
|
199
|
+
size="sm"
|
|
200
|
+
idleLabel="Ask agent to debug"
|
|
201
|
+
loadingLabel="Loading..."
|
|
202
|
+
className="h-7 px-2.5"
|
|
203
|
+
/>`
|
|
204
|
+
: null}
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
`
|
|
209
|
+
: null}
|
|
210
|
+
</details>
|
|
211
|
+
`;
|
|
212
|
+
})}
|
|
213
|
+
</div>
|
|
214
|
+
`}
|
|
215
|
+
<${AgentSendModal}
|
|
216
|
+
visible=${!!debugRequest}
|
|
217
|
+
title="Ask agent to debug"
|
|
218
|
+
messageLabel="Debug request"
|
|
219
|
+
messageRows=${12}
|
|
220
|
+
initialMessage=${debugAgentMessage}
|
|
221
|
+
resetKey=${String(debugRequest?.id || "")}
|
|
222
|
+
submitLabel="Send debug request"
|
|
223
|
+
loadingLabel="Sending..."
|
|
224
|
+
onClose=${() => actions.setDebugRequest(null)}
|
|
225
|
+
onSubmit=${async ({ selectedSessionKey, message }) => {
|
|
226
|
+
try {
|
|
227
|
+
await sendAgentMessage({
|
|
228
|
+
message,
|
|
229
|
+
sessionKey: selectedSessionKey,
|
|
230
|
+
});
|
|
231
|
+
showToast("Debug request sent to agent", "success");
|
|
232
|
+
return true;
|
|
233
|
+
} catch (err) {
|
|
234
|
+
showToast(err.message || "Could not send debug request", "error");
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
}}
|
|
238
|
+
/>
|
|
239
|
+
</div>
|
|
240
|
+
`;
|
|
241
|
+
};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from "https://esm.sh/preact/hooks";
|
|
2
|
+
import {
|
|
3
|
+
fetchWebhookRequest,
|
|
4
|
+
fetchWebhookRequests,
|
|
5
|
+
} from "../../../lib/api.js";
|
|
6
|
+
import { usePolling } from "../../../hooks/usePolling.js";
|
|
7
|
+
import { showToast } from "../../toast.js";
|
|
8
|
+
|
|
9
|
+
export const useRequestHistory = ({
|
|
10
|
+
selectedHookName = "",
|
|
11
|
+
effectiveAuthMode = "headers",
|
|
12
|
+
webhookUrl = "",
|
|
13
|
+
webhookUrlWithQueryToken = "",
|
|
14
|
+
bearerTokenValue = "",
|
|
15
|
+
}) => {
|
|
16
|
+
const [statusFilter, setStatusFilter] = useState("all");
|
|
17
|
+
const [expandedRows, setExpandedRows] = useState(() => new Set());
|
|
18
|
+
const [replayingRequestId, setReplayingRequestId] = useState(null);
|
|
19
|
+
const [debugLoadingRequestId, setDebugLoadingRequestId] = useState(null);
|
|
20
|
+
const [debugRequest, setDebugRequest] = useState(null);
|
|
21
|
+
|
|
22
|
+
const requestsPoll = usePolling(
|
|
23
|
+
async () => {
|
|
24
|
+
if (!selectedHookName) return { requests: [] };
|
|
25
|
+
const data = await fetchWebhookRequests(selectedHookName, {
|
|
26
|
+
limit: 25,
|
|
27
|
+
offset: 0,
|
|
28
|
+
status: statusFilter,
|
|
29
|
+
});
|
|
30
|
+
return data;
|
|
31
|
+
},
|
|
32
|
+
5000,
|
|
33
|
+
{ enabled: !!selectedHookName },
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const requests = requestsPoll.data?.requests || [];
|
|
37
|
+
|
|
38
|
+
const handleRequestRowToggle = useCallback((id, isOpen) => {
|
|
39
|
+
setExpandedRows((prev) => {
|
|
40
|
+
const next = new Set(prev);
|
|
41
|
+
if (isOpen) next.add(id);
|
|
42
|
+
else next.delete(id);
|
|
43
|
+
return next;
|
|
44
|
+
});
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
const handleSetStatusFilter = useCallback(
|
|
48
|
+
(filter) => {
|
|
49
|
+
setStatusFilter(filter);
|
|
50
|
+
setExpandedRows(new Set());
|
|
51
|
+
setTimeout(() => requestsPoll.refresh(), 0);
|
|
52
|
+
},
|
|
53
|
+
[requestsPoll.refresh],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const resetState = useCallback(() => {
|
|
57
|
+
setStatusFilter("all");
|
|
58
|
+
setExpandedRows(new Set());
|
|
59
|
+
setDebugRequest(null);
|
|
60
|
+
setDebugLoadingRequestId(null);
|
|
61
|
+
setReplayingRequestId(null);
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
const handleCopyRequestField = useCallback(async (value, label) => {
|
|
65
|
+
try {
|
|
66
|
+
await navigator.clipboard.writeText(String(value || ""));
|
|
67
|
+
showToast(`${label} copied`, "success");
|
|
68
|
+
} catch {
|
|
69
|
+
showToast(
|
|
70
|
+
`Could not copy ${String(label || "value").toLowerCase()}`,
|
|
71
|
+
"error",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
const requestUrl = useMemo(() => {
|
|
77
|
+
return effectiveAuthMode === "query" ? webhookUrlWithQueryToken : webhookUrl;
|
|
78
|
+
}, [effectiveAuthMode, webhookUrl, webhookUrlWithQueryToken]);
|
|
79
|
+
|
|
80
|
+
const requestHeaders = useMemo(() => {
|
|
81
|
+
const headers = { "Content-Type": "application/json" };
|
|
82
|
+
if (effectiveAuthMode === "headers") {
|
|
83
|
+
headers.Authorization = bearerTokenValue;
|
|
84
|
+
}
|
|
85
|
+
return headers;
|
|
86
|
+
}, [bearerTokenValue, effectiveAuthMode]);
|
|
87
|
+
|
|
88
|
+
const handleReplayRequest = useCallback(
|
|
89
|
+
async (item) => {
|
|
90
|
+
if (!item || replayingRequestId === item.id) return;
|
|
91
|
+
if (item.payloadTruncated) {
|
|
92
|
+
showToast("Cannot replay a truncated payload", "warning");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
setReplayingRequestId(item.id);
|
|
96
|
+
try {
|
|
97
|
+
const response = await fetch(requestUrl, {
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: requestHeaders,
|
|
100
|
+
body: String(item.payload || ""),
|
|
101
|
+
});
|
|
102
|
+
const bodyText = await response.text();
|
|
103
|
+
let body = null;
|
|
104
|
+
try {
|
|
105
|
+
body = bodyText ? JSON.parse(bodyText) : null;
|
|
106
|
+
} catch {
|
|
107
|
+
body = null;
|
|
108
|
+
}
|
|
109
|
+
const errorMessage =
|
|
110
|
+
body?.ok === false
|
|
111
|
+
? body?.error || "Webhook rejected"
|
|
112
|
+
: !response.ok
|
|
113
|
+
? body?.error || bodyText || `HTTP ${response.status}`
|
|
114
|
+
: "";
|
|
115
|
+
if (errorMessage) {
|
|
116
|
+
showToast(`Replay failed: ${errorMessage}`, "error");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
showToast("Request replayed", "success");
|
|
120
|
+
setTimeout(() => requestsPoll.refresh(), 0);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
showToast(err.message || "Could not replay request", "error");
|
|
123
|
+
} finally {
|
|
124
|
+
setReplayingRequestId(null);
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
[replayingRequestId, requestHeaders, requestUrl, requestsPoll.refresh],
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const handleAskAgentToDebug = useCallback(
|
|
131
|
+
async (item) => {
|
|
132
|
+
if (!selectedHookName || !item?.id || debugLoadingRequestId === item.id)
|
|
133
|
+
return;
|
|
134
|
+
try {
|
|
135
|
+
setDebugLoadingRequestId(item.id);
|
|
136
|
+
const data = await fetchWebhookRequest(selectedHookName, item.id);
|
|
137
|
+
setDebugRequest(data?.request || item);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
showToast(err.message || "Could not load webhook request details", "error");
|
|
140
|
+
} finally {
|
|
141
|
+
setDebugLoadingRequestId(null);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
[debugLoadingRequestId, selectedHookName],
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
state: {
|
|
149
|
+
requests,
|
|
150
|
+
statusFilter,
|
|
151
|
+
expandedRows,
|
|
152
|
+
replayingRequestId,
|
|
153
|
+
debugLoadingRequestId,
|
|
154
|
+
debugRequest,
|
|
155
|
+
},
|
|
156
|
+
actions: {
|
|
157
|
+
refreshRequests: requestsPoll.refresh,
|
|
158
|
+
handleRequestRowToggle,
|
|
159
|
+
handleSetStatusFilter,
|
|
160
|
+
handleReplayRequest,
|
|
161
|
+
handleCopyRequestField,
|
|
162
|
+
handleAskAgentToDebug,
|
|
163
|
+
setDebugRequest,
|
|
164
|
+
resetState,
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
};
|