@chrysb/alphaclaw 0.8.1-beta.1 → 0.8.1-beta.3
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/components/google/gmail-setup-wizard.js +1 -1
- package/lib/public/js/components/session-select-field.js +3 -2
- package/lib/public/js/components/webhooks/request-history/index.js +2 -0
- package/lib/public/js/components/webhooks/request-history/use-request-history.js +7 -1
- package/lib/public/js/components/webhooks/webhook-detail/index.js +86 -12
- package/lib/public/js/components/webhooks/webhook-detail/use-webhook-detail.js +226 -24
- package/lib/public/js/hooks/use-destination-session-selection.js +7 -22
- package/lib/public/js/hooks/useAgentSessions.js +9 -9
- package/lib/public/js/lib/api.js +14 -0
- package/lib/public/js/lib/session-keys.js +37 -0
- package/lib/server/onboarding/index.js +2 -1
- package/lib/server/routes/system.js +47 -11
- package/lib/server/routes/webhooks.js +81 -1
- package/lib/server/webhooks.js +46 -0
- package/package.json +1 -1
|
@@ -15,10 +15,10 @@ import { sendAgentMessage } from "../../lib/api.js";
|
|
|
15
15
|
import { showToast } from "../toast.js";
|
|
16
16
|
import { useAgentSessions } from "../../hooks/useAgentSessions.js";
|
|
17
17
|
import {
|
|
18
|
-
kDestinationSessionFilter,
|
|
19
18
|
kNoDestinationSessionValue,
|
|
20
19
|
useDestinationSessionSelection,
|
|
21
20
|
} from "../../hooks/use-destination-session-selection.js";
|
|
21
|
+
import { kDestinationSessionFilter } from "../../lib/session-keys.js";
|
|
22
22
|
|
|
23
23
|
const html = htm.bind(h);
|
|
24
24
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
2
|
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { getSessionRowKey } from "../lib/session-keys.js";
|
|
3
4
|
|
|
4
5
|
const html = htm.bind(h);
|
|
5
6
|
|
|
@@ -47,8 +48,8 @@ export const SessionSelectField = ({
|
|
|
47
48
|
: null}
|
|
48
49
|
${sessions.map(
|
|
49
50
|
(sessionRow) => html`
|
|
50
|
-
<option value=${
|
|
51
|
-
${String(sessionRow?.label || sessionRow
|
|
51
|
+
<option value=${getSessionRowKey(sessionRow)}>
|
|
52
|
+
${String(sessionRow?.label || getSessionRowKey(sessionRow) || "Session")}
|
|
52
53
|
</option>
|
|
53
54
|
`,
|
|
54
55
|
)}
|
|
@@ -24,6 +24,7 @@ export const RequestHistory = ({
|
|
|
24
24
|
webhookUrlWithQueryToken = "",
|
|
25
25
|
bearerTokenValue = "",
|
|
26
26
|
selectedWebhook = null,
|
|
27
|
+
refreshNonce = 0,
|
|
27
28
|
}) => {
|
|
28
29
|
const { state, actions } = useRequestHistory({
|
|
29
30
|
selectedHookName,
|
|
@@ -31,6 +32,7 @@ export const RequestHistory = ({
|
|
|
31
32
|
webhookUrl,
|
|
32
33
|
webhookUrlWithQueryToken,
|
|
33
34
|
bearerTokenValue,
|
|
35
|
+
refreshNonce,
|
|
34
36
|
});
|
|
35
37
|
|
|
36
38
|
const {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useMemo, useState } from "https://esm.sh/preact/hooks";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "https://esm.sh/preact/hooks";
|
|
2
2
|
import {
|
|
3
3
|
fetchWebhookRequest,
|
|
4
4
|
fetchWebhookRequests,
|
|
@@ -12,6 +12,7 @@ export const useRequestHistory = ({
|
|
|
12
12
|
webhookUrl = "",
|
|
13
13
|
webhookUrlWithQueryToken = "",
|
|
14
14
|
bearerTokenValue = "",
|
|
15
|
+
refreshNonce = 0,
|
|
15
16
|
}) => {
|
|
16
17
|
const [statusFilter, setStatusFilter] = useState("all");
|
|
17
18
|
const [expandedRows, setExpandedRows] = useState(() => new Set());
|
|
@@ -35,6 +36,11 @@ export const useRequestHistory = ({
|
|
|
35
36
|
|
|
36
37
|
const requests = requestsPoll.data?.requests || [];
|
|
37
38
|
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!selectedHookName) return;
|
|
41
|
+
requestsPoll.refresh();
|
|
42
|
+
}, [refreshNonce, requestsPoll.refresh, selectedHookName]);
|
|
43
|
+
|
|
38
44
|
const handleRequestRowToggle = useCallback((id, isOpen) => {
|
|
39
45
|
setExpandedRows((prev) => {
|
|
40
46
|
const next = new Set(prev);
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useCallback, useState } from "https://esm.sh/preact/hooks";
|
|
2
3
|
import htm from "https://esm.sh/htm";
|
|
3
4
|
import { ActionButton } from "../../action-button.js";
|
|
4
5
|
import { Badge } from "../../badge.js";
|
|
5
6
|
import { ConfirmDialog } from "../../confirm-dialog.js";
|
|
6
7
|
import { showToast } from "../../toast.js";
|
|
8
|
+
import { kNoDestinationSessionValue } from "../../../hooks/use-destination-session-selection.js";
|
|
9
|
+
import { getSessionRowKey } from "../../../lib/session-keys.js";
|
|
7
10
|
import { formatDateTime } from "../helpers.js";
|
|
8
11
|
import { RequestHistory } from "../request-history/index.js";
|
|
9
12
|
import { useWebhookDetail } from "./use-webhook-detail.js";
|
|
@@ -16,18 +19,31 @@ export const WebhookDetail = ({
|
|
|
16
19
|
onRestartRequired = () => {},
|
|
17
20
|
onOpenFile = () => {},
|
|
18
21
|
}) => {
|
|
22
|
+
const [historyRefreshNonce, setHistoryRefreshNonce] = useState(0);
|
|
23
|
+
const handleTestWebhookSent = useCallback(() => {
|
|
24
|
+
setHistoryRefreshNonce((value) => value + 1);
|
|
25
|
+
}, []);
|
|
19
26
|
const { state, actions } = useWebhookDetail({
|
|
20
27
|
selectedHookName,
|
|
21
28
|
onBackToList,
|
|
22
29
|
onRestartRequired,
|
|
30
|
+
onTestWebhookSent: handleTestWebhookSent,
|
|
23
31
|
});
|
|
24
32
|
|
|
25
33
|
const {
|
|
26
34
|
authMode,
|
|
27
35
|
selectedWebhook,
|
|
36
|
+
isWebhookLoading,
|
|
37
|
+
webhookLoadError,
|
|
28
38
|
selectedWebhookManaged,
|
|
29
39
|
selectedDeliveryAgentName,
|
|
30
40
|
selectedDeliveryChannel,
|
|
41
|
+
selectableSessions,
|
|
42
|
+
loadingDestinationSessions,
|
|
43
|
+
destinationLoadError,
|
|
44
|
+
destinationSessionKey,
|
|
45
|
+
destinationDirty,
|
|
46
|
+
savingDestination,
|
|
31
47
|
webhookUrl,
|
|
32
48
|
oauthCallbackUrl,
|
|
33
49
|
hasOauthCallback,
|
|
@@ -53,9 +69,19 @@ export const WebhookDetail = ({
|
|
|
53
69
|
</h2>
|
|
54
70
|
</div>
|
|
55
71
|
|
|
56
|
-
${
|
|
57
|
-
?
|
|
58
|
-
|
|
72
|
+
${isWebhookLoading
|
|
73
|
+
? html`<div class="bg-black/20 border border-border rounded-lg p-3">
|
|
74
|
+
<p class="text-xs text-gray-500">Loading webhook details...</p>
|
|
75
|
+
</div>`
|
|
76
|
+
: webhookLoadError
|
|
77
|
+
? html`<div class="bg-black/20 border border-border rounded-lg p-3">
|
|
78
|
+
<p class="text-xs text-red-300">
|
|
79
|
+
${webhookLoadError?.message || "Could not load webhook details"}
|
|
80
|
+
</p>
|
|
81
|
+
</div>`
|
|
82
|
+
: hasOauthCallback
|
|
83
|
+
? null
|
|
84
|
+
: html`<div class="bg-black/20 border border-border rounded-lg p-3 space-y-4">
|
|
59
85
|
${selectedWebhookManaged
|
|
60
86
|
? null
|
|
61
87
|
: html`
|
|
@@ -152,7 +178,7 @@ export const WebhookDetail = ({
|
|
|
152
178
|
`}
|
|
153
179
|
</div>`}
|
|
154
180
|
|
|
155
|
-
${selectedWebhookManaged || !hasOauthCallback
|
|
181
|
+
${isWebhookLoading || webhookLoadError || selectedWebhookManaged || !hasOauthCallback
|
|
156
182
|
? null
|
|
157
183
|
: html`
|
|
158
184
|
<div class="bg-black/20 border border-border rounded-lg p-3 space-y-2">
|
|
@@ -210,13 +236,60 @@ export const WebhookDetail = ({
|
|
|
210
236
|
`}
|
|
211
237
|
|
|
212
238
|
<div class="bg-black/20 border border-border rounded-lg p-3 space-y-2">
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
239
|
+
${selectedWebhookManaged
|
|
240
|
+
? html`
|
|
241
|
+
<p class="text-xs text-gray-500">Deliver to</p>
|
|
242
|
+
<p class="text-xs text-gray-200 font-mono">
|
|
243
|
+
${selectedDeliveryAgentName}${" "}
|
|
244
|
+
<span class="text-xs text-gray-500 font-mono"
|
|
245
|
+
>(${selectedDeliveryChannel})</span
|
|
246
|
+
>
|
|
247
|
+
</p>
|
|
248
|
+
`
|
|
249
|
+
: html`
|
|
250
|
+
<p class="text-xs text-gray-500">Deliver to</p>
|
|
251
|
+
<div class="flex items-center gap-2">
|
|
252
|
+
<select
|
|
253
|
+
value=${destinationSessionKey || kNoDestinationSessionValue}
|
|
254
|
+
onInput=${(event) => {
|
|
255
|
+
const nextValue = String(event.currentTarget?.value || "");
|
|
256
|
+
actions.setDestinationSessionKey(
|
|
257
|
+
nextValue === kNoDestinationSessionValue ? "" : nextValue,
|
|
258
|
+
);
|
|
259
|
+
}}
|
|
260
|
+
disabled=${loadingDestinationSessions || savingDestination}
|
|
261
|
+
class="h-8 flex-1 bg-black/30 border border-border rounded-lg px-3 text-xs text-gray-200 focus:border-gray-500"
|
|
262
|
+
>
|
|
263
|
+
<option value=${kNoDestinationSessionValue}>Default</option>
|
|
264
|
+
${loadingDestinationSessions
|
|
265
|
+
? html`<option value="" disabled>Loading...</option>`
|
|
266
|
+
: selectableSessions.map(
|
|
267
|
+
(sessionRow) => html`
|
|
268
|
+
<option value=${getSessionRowKey(sessionRow)}>
|
|
269
|
+
${String(
|
|
270
|
+
sessionRow?.label ||
|
|
271
|
+
getSessionRowKey(sessionRow) ||
|
|
272
|
+
"Session",
|
|
273
|
+
)}
|
|
274
|
+
</option>
|
|
275
|
+
`,
|
|
276
|
+
)}
|
|
277
|
+
</select>
|
|
278
|
+
<${ActionButton}
|
|
279
|
+
onClick=${actions.handleSaveDestination}
|
|
280
|
+
disabled=${!destinationDirty || savingDestination}
|
|
281
|
+
loading=${savingDestination}
|
|
282
|
+
tone="secondary"
|
|
283
|
+
size="sm"
|
|
284
|
+
idleLabel="Save"
|
|
285
|
+
loadingLabel="Saving..."
|
|
286
|
+
className="px-2.5 py-1"
|
|
287
|
+
/>
|
|
288
|
+
</div>
|
|
289
|
+
${destinationLoadError
|
|
290
|
+
? html`<p class="text-xs text-red-400">${destinationLoadError}</p>`
|
|
291
|
+
: null}
|
|
292
|
+
`}
|
|
220
293
|
</div>
|
|
221
294
|
|
|
222
295
|
<div class="bg-black/20 border border-border rounded-lg p-3 space-y-2">
|
|
@@ -304,7 +377,7 @@ export const WebhookDetail = ({
|
|
|
304
377
|
</div>
|
|
305
378
|
</div>
|
|
306
379
|
|
|
307
|
-
${selectedWebhookManaged
|
|
380
|
+
${selectedWebhookManaged && !isWebhookLoading && !webhookLoadError
|
|
308
381
|
? html`
|
|
309
382
|
<div class="rounded-lg border border-yellow-500/30 bg-yellow-500/10 p-3">
|
|
310
383
|
<p class="text-xs text-yellow-200">
|
|
@@ -321,6 +394,7 @@ export const WebhookDetail = ({
|
|
|
321
394
|
webhookUrl=${webhookUrl}
|
|
322
395
|
webhookUrlWithQueryToken=${webhookUrlWithQueryToken}
|
|
323
396
|
bearerTokenValue=${bearerTokenValue}
|
|
397
|
+
refreshNonce=${historyRefreshNonce}
|
|
324
398
|
/>
|
|
325
399
|
<${ConfirmDialog}
|
|
326
400
|
visible=${showRotateOauthConfirm &&
|
|
@@ -1,18 +1,72 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useState,
|
|
6
|
+
} from "https://esm.sh/preact/hooks";
|
|
2
7
|
import {
|
|
3
8
|
deleteWebhook,
|
|
4
9
|
fetchAgents,
|
|
5
10
|
fetchWebhookDetail,
|
|
6
11
|
rotateWebhookOauthCallback,
|
|
12
|
+
updateWebhookDestination,
|
|
7
13
|
} from "../../../lib/api.js";
|
|
8
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
useDestinationSessionSelection,
|
|
16
|
+
} from "../../../hooks/use-destination-session-selection.js";
|
|
17
|
+
import { useCachedFetch } from "../../../hooks/use-cached-fetch.js";
|
|
18
|
+
import {
|
|
19
|
+
getAgentIdFromSessionKey,
|
|
20
|
+
getSessionRowKey,
|
|
21
|
+
} from "../../../lib/session-keys.js";
|
|
9
22
|
import { showToast } from "../../toast.js";
|
|
10
23
|
import { formatAgentFallbackName } from "../helpers.js";
|
|
11
24
|
|
|
25
|
+
const getWebhookDestination = (webhook = null) => {
|
|
26
|
+
const channel = String(webhook?.channel || "").trim();
|
|
27
|
+
const to = String(webhook?.to || "").trim();
|
|
28
|
+
if (!channel || !to) return null;
|
|
29
|
+
const agentId = String(webhook?.agentId || "").trim();
|
|
30
|
+
return {
|
|
31
|
+
channel,
|
|
32
|
+
to,
|
|
33
|
+
...(agentId ? { agentId } : {}),
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const findDestinationSessionKey = (sessions = [], webhook = null) => {
|
|
38
|
+
const destination = getWebhookDestination(webhook);
|
|
39
|
+
if (!destination) return "";
|
|
40
|
+
const destinationAgentId = String(destination?.agentId || "").trim();
|
|
41
|
+
const matchingSession = sessions.find((sessionRow) => {
|
|
42
|
+
const channel = String(sessionRow?.replyChannel || "").trim();
|
|
43
|
+
const to = String(sessionRow?.replyTo || "").trim();
|
|
44
|
+
const agentId = getAgentIdFromSessionKey(getSessionRowKey(sessionRow));
|
|
45
|
+
const agentMatches = destinationAgentId ? agentId === destinationAgentId : true;
|
|
46
|
+
return (
|
|
47
|
+
channel === destination.channel &&
|
|
48
|
+
to === destination.to &&
|
|
49
|
+
agentMatches
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
return String(matchingSession?.key || "").trim();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const areDestinationsEqual = (left = null, right = null) => {
|
|
56
|
+
if (!left && !right) return true;
|
|
57
|
+
if (!left || !right) return false;
|
|
58
|
+
return (
|
|
59
|
+
String(left.channel || "").trim() === String(right.channel || "").trim() &&
|
|
60
|
+
String(left.to || "").trim() === String(right.to || "").trim() &&
|
|
61
|
+
String(left.agentId || "").trim() === String(right.agentId || "").trim()
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
12
65
|
export const useWebhookDetail = ({
|
|
13
66
|
selectedHookName = "",
|
|
14
67
|
onBackToList = () => {},
|
|
15
68
|
onRestartRequired = () => {},
|
|
69
|
+
onTestWebhookSent = () => {},
|
|
16
70
|
}) => {
|
|
17
71
|
const [authMode, setAuthMode] = useState("headers");
|
|
18
72
|
const [deleting, setDeleting] = useState(false);
|
|
@@ -21,19 +75,32 @@ export const useWebhookDetail = ({
|
|
|
21
75
|
const [rotatingOauthCallback, setRotatingOauthCallback] = useState(false);
|
|
22
76
|
const [showRotateOauthConfirm, setShowRotateOauthConfirm] = useState(false);
|
|
23
77
|
const [sendingTestWebhook, setSendingTestWebhook] = useState(false);
|
|
78
|
+
const [savingDestination, setSavingDestination] = useState(false);
|
|
24
79
|
|
|
25
|
-
const
|
|
80
|
+
const detailCacheKey = useMemo(
|
|
81
|
+
() => `/api/webhooks/${encodeURIComponent(String(selectedHookName || ""))}`,
|
|
82
|
+
[selectedHookName],
|
|
83
|
+
);
|
|
84
|
+
const detailFetchState = useCachedFetch(
|
|
85
|
+
detailCacheKey,
|
|
26
86
|
async () => {
|
|
27
87
|
if (!selectedHookName) return null;
|
|
28
88
|
const data = await fetchWebhookDetail(selectedHookName);
|
|
29
89
|
return data.webhook || null;
|
|
30
90
|
},
|
|
31
|
-
|
|
32
|
-
|
|
91
|
+
{
|
|
92
|
+
enabled: !!selectedHookName,
|
|
93
|
+
maxAgeMs: 15000,
|
|
94
|
+
},
|
|
33
95
|
);
|
|
96
|
+
const agentsFetchState = useCachedFetch("/api/agents", fetchAgents, {
|
|
97
|
+
enabled: true,
|
|
98
|
+
maxAgeMs: 30000,
|
|
99
|
+
});
|
|
34
100
|
|
|
35
|
-
const
|
|
36
|
-
|
|
101
|
+
const agents = Array.isArray(agentsFetchState.data?.agents)
|
|
102
|
+
? agentsFetchState.data.agents
|
|
103
|
+
: [];
|
|
37
104
|
const agentNameById = useMemo(
|
|
38
105
|
() =>
|
|
39
106
|
new Map(
|
|
@@ -45,7 +112,9 @@ export const useWebhookDetail = ({
|
|
|
45
112
|
[agents],
|
|
46
113
|
);
|
|
47
114
|
|
|
48
|
-
const selectedWebhook =
|
|
115
|
+
const selectedWebhook = detailFetchState.data;
|
|
116
|
+
const isWebhookLoading = !!selectedHookName && detailFetchState.loading;
|
|
117
|
+
const webhookLoadError = detailFetchState.error;
|
|
49
118
|
const selectedWebhookManaged = Boolean(selectedWebhook?.managed);
|
|
50
119
|
const selectedDeliveryAgentId =
|
|
51
120
|
String(selectedWebhook?.agentId || "main").trim() || "main";
|
|
@@ -54,10 +123,57 @@ export const useWebhookDetail = ({
|
|
|
54
123
|
formatAgentFallbackName(selectedDeliveryAgentId);
|
|
55
124
|
const selectedDeliveryChannel =
|
|
56
125
|
String(selectedWebhook?.channel || "last").trim() || "last";
|
|
126
|
+
const destinationResetKey = useMemo(
|
|
127
|
+
() =>
|
|
128
|
+
[
|
|
129
|
+
selectedHookName,
|
|
130
|
+
selectedWebhook?.agentId,
|
|
131
|
+
selectedWebhook?.channel,
|
|
132
|
+
selectedWebhook?.to,
|
|
133
|
+
]
|
|
134
|
+
.map((value) => String(value || "").trim())
|
|
135
|
+
.join("|"),
|
|
136
|
+
[
|
|
137
|
+
selectedHookName,
|
|
138
|
+
selectedWebhook?.agentId,
|
|
139
|
+
selectedWebhook?.channel,
|
|
140
|
+
selectedWebhook?.to,
|
|
141
|
+
],
|
|
142
|
+
);
|
|
143
|
+
const {
|
|
144
|
+
sessions: selectableSessions,
|
|
145
|
+
loading: loadingDestinationSessions,
|
|
146
|
+
error: destinationLoadError,
|
|
147
|
+
destinationSessionKey,
|
|
148
|
+
setDestinationSessionKey,
|
|
149
|
+
selectedDestination,
|
|
150
|
+
} = useDestinationSessionSelection({
|
|
151
|
+
enabled: !!selectedHookName && !selectedWebhookManaged,
|
|
152
|
+
resetKey: destinationResetKey,
|
|
153
|
+
});
|
|
57
154
|
|
|
58
155
|
const webhookUrl = selectedWebhook?.fullUrl || `.../hooks/${selectedHookName}`;
|
|
59
156
|
const oauthCallbackUrl = String(selectedWebhook?.oauthCallbackUrl || "").trim();
|
|
60
157
|
const hasOauthCallback = !!oauthCallbackUrl;
|
|
158
|
+
const oauthCallbackTestUrl = useMemo(() => {
|
|
159
|
+
if (!hasOauthCallback) return "";
|
|
160
|
+
try {
|
|
161
|
+
const url = new URL(oauthCallbackUrl);
|
|
162
|
+
if (!url.searchParams.has("code")) {
|
|
163
|
+
url.searchParams.set("code", "TEST_AUTH_CODE");
|
|
164
|
+
}
|
|
165
|
+
if (!url.searchParams.has("state")) {
|
|
166
|
+
url.searchParams.set("state", "TEST_STATE");
|
|
167
|
+
}
|
|
168
|
+
if (!url.searchParams.has("message")) {
|
|
169
|
+
url.searchParams.set("message", "OAuth callback test");
|
|
170
|
+
}
|
|
171
|
+
return url.toString();
|
|
172
|
+
} catch {
|
|
173
|
+
const separator = oauthCallbackUrl.includes("?") ? "&" : "?";
|
|
174
|
+
return `${oauthCallbackUrl}${separator}code=TEST_AUTH_CODE&state=TEST_STATE&message=OAuth%20callback%20test`;
|
|
175
|
+
}
|
|
176
|
+
}, [hasOauthCallback, oauthCallbackUrl]);
|
|
61
177
|
const webhookUrlWithQueryToken =
|
|
62
178
|
selectedWebhook?.queryStringUrl ||
|
|
63
179
|
`${webhookUrl}${webhookUrl.includes("?") ? "&" : "?"}token=<WEBHOOK_TOKEN>`;
|
|
@@ -119,31 +235,104 @@ export const useWebhookDetail = ({
|
|
|
119
235
|
`curl -X POST "${webhookUrlWithQueryToken}" ` +
|
|
120
236
|
`-H "Content-Type: application/json" ` +
|
|
121
237
|
`-d '${webhookTestPayloadJson}'`;
|
|
238
|
+
const curlCommandOauth = `curl -X GET "${oauthCallbackTestUrl}"`;
|
|
122
239
|
|
|
123
240
|
const effectiveAuthMode = selectedWebhookManaged ? "headers" : authMode;
|
|
124
|
-
const activeCurlCommand =
|
|
125
|
-
|
|
241
|
+
const activeCurlCommand = hasOauthCallback
|
|
242
|
+
? curlCommandOauth
|
|
243
|
+
: effectiveAuthMode === "query"
|
|
244
|
+
? curlCommandQuery
|
|
245
|
+
: curlCommandHeaders;
|
|
126
246
|
|
|
127
247
|
const refreshDetail = useCallback(() => {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}, [
|
|
248
|
+
detailFetchState.refresh({ force: true });
|
|
249
|
+
agentsFetchState.refresh({ force: true });
|
|
250
|
+
}, [agentsFetchState.refresh, detailFetchState.refresh]);
|
|
251
|
+
|
|
252
|
+
useEffect(() => {
|
|
253
|
+
if (!selectedHookName || selectedWebhookManaged || !selectedWebhook) return;
|
|
254
|
+
if (!Array.isArray(selectableSessions) || selectableSessions.length <= 0) {
|
|
255
|
+
setDestinationSessionKey("");
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const nextKey = findDestinationSessionKey(selectableSessions, selectedWebhook);
|
|
259
|
+
setDestinationSessionKey(nextKey);
|
|
260
|
+
}, [
|
|
261
|
+
selectableSessions,
|
|
262
|
+
selectedHookName,
|
|
263
|
+
selectedWebhook,
|
|
264
|
+
selectedWebhookManaged,
|
|
265
|
+
setDestinationSessionKey,
|
|
266
|
+
]);
|
|
267
|
+
|
|
268
|
+
const currentDestination = useMemo(
|
|
269
|
+
() => getWebhookDestination(selectedWebhook),
|
|
270
|
+
[selectedWebhook],
|
|
271
|
+
);
|
|
272
|
+
const destinationDirty = useMemo(
|
|
273
|
+
() => !areDestinationsEqual(currentDestination, selectedDestination),
|
|
274
|
+
[currentDestination, selectedDestination],
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const handleSaveDestination = useCallback(async () => {
|
|
278
|
+
if (
|
|
279
|
+
!selectedHookName ||
|
|
280
|
+
selectedWebhookManaged ||
|
|
281
|
+
savingDestination ||
|
|
282
|
+
!destinationDirty
|
|
283
|
+
) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
setSavingDestination(true);
|
|
287
|
+
try {
|
|
288
|
+
const data = await updateWebhookDestination(selectedHookName, {
|
|
289
|
+
destination: selectedDestination || null,
|
|
290
|
+
});
|
|
291
|
+
if (data?.restartRequired) {
|
|
292
|
+
onRestartRequired(true);
|
|
293
|
+
}
|
|
294
|
+
if (data?.syncWarning) {
|
|
295
|
+
showToast(`Updated, but git-sync failed: ${data.syncWarning}`, "warning");
|
|
296
|
+
}
|
|
297
|
+
showToast("Webhook destination updated", "success");
|
|
298
|
+
refreshDetail();
|
|
299
|
+
} catch (err) {
|
|
300
|
+
showToast(err.message || "Could not update webhook destination", "error");
|
|
301
|
+
} finally {
|
|
302
|
+
setSavingDestination(false);
|
|
303
|
+
}
|
|
304
|
+
}, [
|
|
305
|
+
destinationDirty,
|
|
306
|
+
onRestartRequired,
|
|
307
|
+
refreshDetail,
|
|
308
|
+
savingDestination,
|
|
309
|
+
selectedDestination,
|
|
310
|
+
selectedHookName,
|
|
311
|
+
selectedWebhookManaged,
|
|
312
|
+
]);
|
|
131
313
|
|
|
132
314
|
const handleSendTestWebhook = useCallback(async () => {
|
|
133
315
|
if (!selectedHookName || sendingTestWebhook) return;
|
|
134
316
|
setSendingTestWebhook(true);
|
|
135
|
-
const requestUrl =
|
|
136
|
-
effectiveAuthMode === "query" ? webhookUrlWithQueryToken : webhookUrl;
|
|
137
|
-
const headers = { "Content-Type": "application/json" };
|
|
138
|
-
if (effectiveAuthMode === "headers") {
|
|
139
|
-
headers.Authorization = bearerTokenValue;
|
|
140
|
-
}
|
|
141
317
|
try {
|
|
142
|
-
const response =
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
318
|
+
const response = hasOauthCallback
|
|
319
|
+
? await fetch(oauthCallbackTestUrl, {
|
|
320
|
+
method: "GET",
|
|
321
|
+
})
|
|
322
|
+
: await fetch(
|
|
323
|
+
effectiveAuthMode === "query" ? webhookUrlWithQueryToken : webhookUrl,
|
|
324
|
+
{
|
|
325
|
+
method: "POST",
|
|
326
|
+
headers: {
|
|
327
|
+
"Content-Type": "application/json",
|
|
328
|
+
...(effectiveAuthMode === "headers"
|
|
329
|
+
? { Authorization: bearerTokenValue }
|
|
330
|
+
: {}),
|
|
331
|
+
},
|
|
332
|
+
body: webhookTestPayloadJson,
|
|
333
|
+
},
|
|
334
|
+
);
|
|
335
|
+
onTestWebhookSent();
|
|
147
336
|
const bodyText = await response.text();
|
|
148
337
|
let body = null;
|
|
149
338
|
try {
|
|
@@ -170,6 +359,9 @@ export const useWebhookDetail = ({
|
|
|
170
359
|
}, [
|
|
171
360
|
bearerTokenValue,
|
|
172
361
|
effectiveAuthMode,
|
|
362
|
+
hasOauthCallback,
|
|
363
|
+
oauthCallbackTestUrl,
|
|
364
|
+
onTestWebhookSent,
|
|
173
365
|
selectedHookName,
|
|
174
366
|
sendingTestWebhook,
|
|
175
367
|
webhookTestPayloadJson,
|
|
@@ -229,9 +421,17 @@ export const useWebhookDetail = ({
|
|
|
229
421
|
state: {
|
|
230
422
|
authMode,
|
|
231
423
|
selectedWebhook,
|
|
424
|
+
isWebhookLoading,
|
|
425
|
+
webhookLoadError,
|
|
232
426
|
selectedWebhookManaged,
|
|
233
427
|
selectedDeliveryAgentName,
|
|
234
428
|
selectedDeliveryChannel,
|
|
429
|
+
selectableSessions,
|
|
430
|
+
loadingDestinationSessions,
|
|
431
|
+
destinationLoadError,
|
|
432
|
+
destinationSessionKey,
|
|
433
|
+
destinationDirty,
|
|
434
|
+
savingDestination,
|
|
235
435
|
webhookUrl,
|
|
236
436
|
oauthCallbackUrl,
|
|
237
437
|
hasOauthCallback,
|
|
@@ -250,9 +450,11 @@ export const useWebhookDetail = ({
|
|
|
250
450
|
actions: {
|
|
251
451
|
refreshDetail,
|
|
252
452
|
setAuthMode,
|
|
453
|
+
setDestinationSessionKey,
|
|
253
454
|
setShowDeleteConfirm,
|
|
254
455
|
setDeleteTransformDir,
|
|
255
456
|
setShowRotateOauthConfirm,
|
|
457
|
+
handleSaveDestination,
|
|
256
458
|
handleDeleteConfirmed,
|
|
257
459
|
handleRotateOauthCallback,
|
|
258
460
|
handleSendTestWebhook,
|
|
@@ -1,27 +1,12 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useState } from "https://esm.sh/preact/hooks";
|
|
2
2
|
import { useAgentSessions } from "./useAgentSessions.js";
|
|
3
|
+
import {
|
|
4
|
+
getDestinationFromSession,
|
|
5
|
+
kDestinationSessionFilter,
|
|
6
|
+
} from "../lib/session-keys.js";
|
|
3
7
|
|
|
4
8
|
export const kNoDestinationSessionValue = "__none__";
|
|
5
9
|
|
|
6
|
-
export const kDestinationSessionFilter = (sessionRow) => {
|
|
7
|
-
const key = String(sessionRow?.key || "").toLowerCase();
|
|
8
|
-
return key.includes(":direct:") || key.includes(":group:");
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export const getDestinationFromSession = (sessionRow = null) => {
|
|
12
|
-
const channel = String(sessionRow?.replyChannel || "").trim();
|
|
13
|
-
const to = String(sessionRow?.replyTo || "").trim();
|
|
14
|
-
if (!channel || !to) return null;
|
|
15
|
-
const key = String(sessionRow?.key || "").trim();
|
|
16
|
-
const agentMatch = key.match(/^agent:([^:]+):/);
|
|
17
|
-
const agentId = String(agentMatch?.[1] || "").trim();
|
|
18
|
-
return {
|
|
19
|
-
channel,
|
|
20
|
-
to,
|
|
21
|
-
...(agentId ? { agentId } : {}),
|
|
22
|
-
};
|
|
23
|
-
};
|
|
24
|
-
|
|
25
10
|
export const useDestinationSessionSelection = ({
|
|
26
11
|
enabled = false,
|
|
27
12
|
resetKey = "",
|
|
@@ -48,10 +33,10 @@ export const useDestinationSessionSelection = ({
|
|
|
48
33
|
const preferredSessionKey = useMemo(() => {
|
|
49
34
|
const matchingPreferredSession = sessions.find(
|
|
50
35
|
(sessionRow) =>
|
|
51
|
-
|
|
36
|
+
getSessionRowKey(sessionRow) === String(selectedSessionKey || "").trim(),
|
|
52
37
|
);
|
|
53
38
|
return String(
|
|
54
|
-
matchingPreferredSession
|
|
39
|
+
getSessionRowKey(matchingPreferredSession) || getSessionRowKey(sessions[0]),
|
|
55
40
|
).trim();
|
|
56
41
|
}, [sessions, selectedSessionKey]);
|
|
57
42
|
|
|
@@ -63,7 +48,7 @@ export const useDestinationSessionSelection = ({
|
|
|
63
48
|
() =>
|
|
64
49
|
sessions.find(
|
|
65
50
|
(sessionRow) =>
|
|
66
|
-
|
|
51
|
+
getSessionRowKey(sessionRow) === String(effectiveSessionKey || "").trim(),
|
|
67
52
|
) || null,
|
|
68
53
|
[effectiveSessionKey, sessions],
|
|
69
54
|
);
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
kAgentSessionsCacheKey,
|
|
5
5
|
kAgentLastSessionKey,
|
|
6
6
|
} from "../lib/storage-keys.js";
|
|
7
|
+
import { getSessionRowKey, isDestinationSessionKey } from "../lib/session-keys.js";
|
|
7
8
|
|
|
8
9
|
const readCachedSessions = () => {
|
|
9
10
|
try {
|
|
@@ -38,14 +39,13 @@ const writeLastSessionKey = (key) => {
|
|
|
38
39
|
|
|
39
40
|
const pickPreferredSession = (sessions, lastKey) => {
|
|
40
41
|
if (lastKey) {
|
|
41
|
-
const lastMatch = sessions.find((row) =>
|
|
42
|
+
const lastMatch = sessions.find((row) => getSessionRowKey(row) === lastKey);
|
|
42
43
|
if (lastMatch) return lastMatch;
|
|
43
44
|
}
|
|
44
45
|
return (
|
|
45
|
-
sessions.find((row) =>
|
|
46
|
+
sessions.find((row) => getSessionRowKey(row).toLowerCase() === "agent:main:main") ||
|
|
46
47
|
sessions.find((row) => {
|
|
47
|
-
|
|
48
|
-
return key.includes(":direct:") || key.includes(":group:");
|
|
48
|
+
return isDestinationSessionKey(getSessionRowKey(row));
|
|
49
49
|
}) ||
|
|
50
50
|
sessions[0] ||
|
|
51
51
|
null
|
|
@@ -81,7 +81,7 @@ export const useAgentSessions = ({ enabled = false, filter } = {}) => {
|
|
|
81
81
|
if (cached.length > 0) {
|
|
82
82
|
setAllSessions(cached);
|
|
83
83
|
const preferred = pickPreferredSession(cached, lastKey);
|
|
84
|
-
setSelectedSessionKeyState(
|
|
84
|
+
setSelectedSessionKeyState(getSessionRowKey(preferred));
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
const load = async () => {
|
|
@@ -95,7 +95,7 @@ export const useAgentSessions = ({ enabled = false, filter } = {}) => {
|
|
|
95
95
|
writeCachedSessions(nextSessions);
|
|
96
96
|
if (cached.length === 0 || !lastKey) {
|
|
97
97
|
const preferred = pickPreferredSession(nextSessions, lastKey);
|
|
98
|
-
setSelectedSessionKeyState(
|
|
98
|
+
setSelectedSessionKeyState(getSessionRowKey(preferred));
|
|
99
99
|
}
|
|
100
100
|
} catch (err) {
|
|
101
101
|
if (!active) return;
|
|
@@ -126,15 +126,15 @@ export const useAgentSessions = ({ enabled = false, filter } = {}) => {
|
|
|
126
126
|
return;
|
|
127
127
|
}
|
|
128
128
|
const hasSelectedSession = sessions.some(
|
|
129
|
-
(row) =>
|
|
129
|
+
(row) => getSessionRowKey(row) === String(selectedSessionKey || ""),
|
|
130
130
|
);
|
|
131
131
|
if (hasSelectedSession) return;
|
|
132
132
|
const preferred = pickPreferredSession(sessions, readLastSessionKey());
|
|
133
|
-
setSelectedSessionKeyState(
|
|
133
|
+
setSelectedSessionKeyState(getSessionRowKey(preferred));
|
|
134
134
|
}, [enabled, sessions, selectedSessionKey]);
|
|
135
135
|
|
|
136
136
|
const selectedSession = useMemo(
|
|
137
|
-
() => sessions.find((row) =>
|
|
137
|
+
() => sessions.find((row) => getSessionRowKey(row) === selectedSessionKey) || null,
|
|
138
138
|
[sessions, selectedSessionKey],
|
|
139
139
|
);
|
|
140
140
|
|
package/lib/public/js/lib/api.js
CHANGED
|
@@ -1158,6 +1158,20 @@ export async function deleteWebhook(name, { deleteTransformDir = false } = {}) {
|
|
|
1158
1158
|
return parseJsonOrThrow(res, "Could not delete webhook");
|
|
1159
1159
|
}
|
|
1160
1160
|
|
|
1161
|
+
export async function updateWebhookDestination(name, { destination = null } = {}) {
|
|
1162
|
+
const res = await authFetch(
|
|
1163
|
+
`/api/webhooks/${encodeURIComponent(name)}/destination`,
|
|
1164
|
+
{
|
|
1165
|
+
method: "PUT",
|
|
1166
|
+
headers: { "Content-Type": "application/json" },
|
|
1167
|
+
body: JSON.stringify({
|
|
1168
|
+
destination,
|
|
1169
|
+
}),
|
|
1170
|
+
},
|
|
1171
|
+
);
|
|
1172
|
+
return parseJsonOrThrow(res, "Could not update webhook destination");
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1161
1175
|
export async function createWebhookOauthCallback(name) {
|
|
1162
1176
|
const res = await authFetch(
|
|
1163
1177
|
`/api/webhooks/${encodeURIComponent(name)}/oauth-callback`,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const getNormalizedSessionKey = (sessionKey = "") =>
|
|
2
|
+
String(sessionKey || "").trim();
|
|
3
|
+
|
|
4
|
+
export const getSessionRowKey = (sessionRow = null) =>
|
|
5
|
+
getNormalizedSessionKey(sessionRow?.key || sessionRow?.sessionKey || "");
|
|
6
|
+
|
|
7
|
+
export const getAgentIdFromSessionKey = (sessionKey = "") => {
|
|
8
|
+
const normalizedSessionKey = getNormalizedSessionKey(sessionKey);
|
|
9
|
+
const agentMatch = normalizedSessionKey.match(/^agent:([^:]+):/);
|
|
10
|
+
return String(agentMatch?.[1] || "").trim();
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const isDestinationSessionKey = (sessionKey = "") => {
|
|
14
|
+
const normalizedSessionKey = getNormalizedSessionKey(sessionKey).toLowerCase();
|
|
15
|
+
return (
|
|
16
|
+
normalizedSessionKey.includes(":direct:") ||
|
|
17
|
+
normalizedSessionKey.includes(":group:")
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const kDestinationSessionFilter = (sessionRow) =>
|
|
22
|
+
!!(
|
|
23
|
+
String(sessionRow?.replyChannel || "").trim() &&
|
|
24
|
+
String(sessionRow?.replyTo || "").trim()
|
|
25
|
+
) || isDestinationSessionKey(getSessionRowKey(sessionRow));
|
|
26
|
+
|
|
27
|
+
export const getDestinationFromSession = (sessionRow = null) => {
|
|
28
|
+
const channel = String(sessionRow?.replyChannel || "").trim();
|
|
29
|
+
const to = String(sessionRow?.replyTo || "").trim();
|
|
30
|
+
if (!channel || !to) return null;
|
|
31
|
+
const agentId = getAgentIdFromSessionKey(getSessionRowKey(sessionRow));
|
|
32
|
+
return {
|
|
33
|
+
channel,
|
|
34
|
+
to,
|
|
35
|
+
...(agentId ? { agentId } : {}),
|
|
36
|
+
};
|
|
37
|
+
};
|
|
@@ -529,7 +529,6 @@ const createOnboardingService = ({
|
|
|
529
529
|
});
|
|
530
530
|
}
|
|
531
531
|
authProfiles?.syncConfigAuthReferencesForAgent?.();
|
|
532
|
-
ensureGatewayProxyConfig(getBaseUrl(req));
|
|
533
532
|
|
|
534
533
|
installGogCliSkill({ fs, openclawDir: OPENCLAW_DIR });
|
|
535
534
|
|
|
@@ -549,6 +548,8 @@ const createOnboardingService = ({
|
|
|
549
548
|
),
|
|
550
549
|
);
|
|
551
550
|
|
|
551
|
+
ensureGatewayProxyConfig(getBaseUrl(req));
|
|
552
|
+
|
|
552
553
|
try {
|
|
553
554
|
const commitMsg = importMode
|
|
554
555
|
? "imported existing setup via AlphaClaw"
|
|
@@ -68,17 +68,48 @@ const registerSystemRoutes = ({
|
|
|
68
68
|
const parseJsonFromStdout = (stdout) => {
|
|
69
69
|
const raw = String(stdout || "").trim();
|
|
70
70
|
if (!raw) return null;
|
|
71
|
-
|
|
72
|
-
(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(raw);
|
|
73
|
+
} catch {}
|
|
74
|
+
const lines = raw
|
|
75
|
+
.split("\n")
|
|
76
|
+
.map((line) => line.trim())
|
|
77
|
+
.filter(Boolean);
|
|
78
|
+
for (const line of lines) {
|
|
79
|
+
if (!(line.startsWith("{") || line.startsWith("["))) continue;
|
|
76
80
|
try {
|
|
77
|
-
return JSON.parse(
|
|
81
|
+
return JSON.parse(line);
|
|
78
82
|
} catch {}
|
|
79
83
|
}
|
|
84
|
+
const candidateStarts = [raw.indexOf("{"), raw.indexOf("[")].filter((idx) => idx >= 0);
|
|
85
|
+
for (const start of candidateStarts) {
|
|
86
|
+
for (let end = raw.length; end > start; end -= 1) {
|
|
87
|
+
const candidate = raw.slice(start, end).trim();
|
|
88
|
+
if (!(candidate.endsWith("}") || candidate.endsWith("]"))) continue;
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(candidate);
|
|
91
|
+
} catch {}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
80
94
|
return null;
|
|
81
95
|
};
|
|
96
|
+
const getRawSessionKey = (sessionRow = {}) =>
|
|
97
|
+
String(sessionRow?.key || sessionRow?.sessionKey || sessionRow?.id || "").trim();
|
|
98
|
+
const getRawSessionsFromPayload = (payload) => {
|
|
99
|
+
if (Array.isArray(payload)) return payload;
|
|
100
|
+
const candidates = [
|
|
101
|
+
payload?.sessions,
|
|
102
|
+
payload?.items,
|
|
103
|
+
payload?.data?.sessions,
|
|
104
|
+
payload?.data?.items,
|
|
105
|
+
payload?.result?.sessions,
|
|
106
|
+
payload?.result?.items,
|
|
107
|
+
];
|
|
108
|
+
for (const candidate of candidates) {
|
|
109
|
+
if (Array.isArray(candidate)) return candidate;
|
|
110
|
+
}
|
|
111
|
+
return [];
|
|
112
|
+
};
|
|
82
113
|
const toTitleWords = (value) =>
|
|
83
114
|
String(value || "")
|
|
84
115
|
.trim()
|
|
@@ -218,7 +249,7 @@ const registerSystemRoutes = ({
|
|
|
218
249
|
throw new Error(result.stderr || "Could not load agent sessions");
|
|
219
250
|
}
|
|
220
251
|
const payload = parseJsonFromStdout(result.stdout);
|
|
221
|
-
const sessions =
|
|
252
|
+
const sessions = getRawSessionsFromPayload(payload);
|
|
222
253
|
const config = readOpenclawConfig({
|
|
223
254
|
fsModule: fs,
|
|
224
255
|
openclawDir: OPENCLAW_DIR,
|
|
@@ -226,7 +257,7 @@ const registerSystemRoutes = ({
|
|
|
226
257
|
});
|
|
227
258
|
return sessions
|
|
228
259
|
.filter((sessionRow) => {
|
|
229
|
-
const key =
|
|
260
|
+
const key = getRawSessionKey(sessionRow).toLowerCase();
|
|
230
261
|
if (!key) return false;
|
|
231
262
|
if (
|
|
232
263
|
key.includes(":hook:") ||
|
|
@@ -238,12 +269,17 @@ const registerSystemRoutes = ({
|
|
|
238
269
|
return true;
|
|
239
270
|
})
|
|
240
271
|
.map((sessionRow) => {
|
|
241
|
-
const key =
|
|
272
|
+
const key = getRawSessionKey(sessionRow);
|
|
242
273
|
const replyTarget = getSessionReplyTarget(key);
|
|
243
274
|
return {
|
|
244
275
|
key,
|
|
245
|
-
sessionId: String(sessionRow?.sessionId || ""),
|
|
246
|
-
updatedAt:
|
|
276
|
+
sessionId: String(sessionRow?.sessionId || sessionRow?.id || ""),
|
|
277
|
+
updatedAt:
|
|
278
|
+
Number(
|
|
279
|
+
sessionRow?.updatedAt ||
|
|
280
|
+
sessionRow?.lastActivityAt ||
|
|
281
|
+
sessionRow?.lastActiveAt,
|
|
282
|
+
) || 0,
|
|
247
283
|
label: buildSessionLabel(sessionRow, config),
|
|
248
284
|
replyChannel: replyTarget.replyChannel,
|
|
249
285
|
replyTo: replyTarget.replyTo,
|
|
@@ -2,6 +2,7 @@ const {
|
|
|
2
2
|
listWebhooks,
|
|
3
3
|
getWebhookDetail,
|
|
4
4
|
createWebhook,
|
|
5
|
+
updateWebhookDestination,
|
|
5
6
|
deleteWebhook,
|
|
6
7
|
validateWebhookName,
|
|
7
8
|
} = require("../webhooks");
|
|
@@ -69,6 +70,36 @@ const buildWebhookUrls = ({ baseUrl, name, oauthCallback = null }) => {
|
|
|
69
70
|
};
|
|
70
71
|
};
|
|
71
72
|
|
|
73
|
+
const buildOauthTransformSource = (name) => {
|
|
74
|
+
return [
|
|
75
|
+
"export default async function transform(payload, context) {",
|
|
76
|
+
" const data = payload.payload || payload || {};",
|
|
77
|
+
" const message = String(data.message || \"\").trim();",
|
|
78
|
+
" const code = String(data.code || \"\").trim();",
|
|
79
|
+
" const state = String(data.state || \"\").trim();",
|
|
80
|
+
" const error = String(data.error || \"\").trim();",
|
|
81
|
+
" const fallbackMessage = error",
|
|
82
|
+
" ? `OAuth callback error: ${error}`",
|
|
83
|
+
" : code",
|
|
84
|
+
" ? \"OAuth callback received (authorization code present)\"",
|
|
85
|
+
" : state",
|
|
86
|
+
" ? \"OAuth callback received (state present)\"",
|
|
87
|
+
" : \"OAuth callback received\";",
|
|
88
|
+
" return {",
|
|
89
|
+
" message: message || fallbackMessage,",
|
|
90
|
+
` name: data.name || \"${name}\",`,
|
|
91
|
+
" wakeMode: data.wakeMode || \"now\",",
|
|
92
|
+
" oauth: {",
|
|
93
|
+
" code,",
|
|
94
|
+
" state,",
|
|
95
|
+
" error,",
|
|
96
|
+
" },",
|
|
97
|
+
" };",
|
|
98
|
+
"}",
|
|
99
|
+
"",
|
|
100
|
+
].join("\n");
|
|
101
|
+
};
|
|
102
|
+
|
|
72
103
|
const registerWebhookRoutes = ({
|
|
73
104
|
app,
|
|
74
105
|
fs,
|
|
@@ -171,7 +202,14 @@ const registerWebhookRoutes = ({
|
|
|
171
202
|
oauthCallback = false,
|
|
172
203
|
} = req.body || {};
|
|
173
204
|
const name = validateWebhookName(rawName);
|
|
174
|
-
const
|
|
205
|
+
const transformSource = oauthCallback ? buildOauthTransformSource(name) : "";
|
|
206
|
+
const webhook = createWebhook({
|
|
207
|
+
fs,
|
|
208
|
+
constants,
|
|
209
|
+
name,
|
|
210
|
+
destination,
|
|
211
|
+
transformSource,
|
|
212
|
+
});
|
|
175
213
|
const oauthCallbackRecord = oauthCallback
|
|
176
214
|
? createOauthCallbackEntry({ hookName: name })
|
|
177
215
|
: null;
|
|
@@ -209,6 +247,48 @@ const registerWebhookRoutes = ({
|
|
|
209
247
|
}
|
|
210
248
|
});
|
|
211
249
|
|
|
250
|
+
app.put("/api/webhooks/:name/destination", async (req, res) => {
|
|
251
|
+
try {
|
|
252
|
+
const name = validateWebhookName(req.params.name);
|
|
253
|
+
const detail = updateWebhookDestination({
|
|
254
|
+
fs,
|
|
255
|
+
constants,
|
|
256
|
+
name,
|
|
257
|
+
destination: req?.body?.destination ?? null,
|
|
258
|
+
});
|
|
259
|
+
const summary = getHookSummaries().find((item) => item.hookName === name);
|
|
260
|
+
const oauthCallback = getOauthCallbackByHookEntry(name);
|
|
261
|
+
const merged = mergeWebhookAndSummary({ webhook: detail, summary });
|
|
262
|
+
const baseUrl = getBaseUrl(req);
|
|
263
|
+
const urls = buildWebhookUrls({ baseUrl, name, oauthCallback });
|
|
264
|
+
const syncWarning = await runWebhookGitSync("update destination", name);
|
|
265
|
+
markRestartRequired("webhooks");
|
|
266
|
+
const snapshot = await getRestartSnapshot();
|
|
267
|
+
return res.json({
|
|
268
|
+
ok: true,
|
|
269
|
+
webhook: {
|
|
270
|
+
...merged,
|
|
271
|
+
fullUrl: urls.fullUrl,
|
|
272
|
+
queryStringUrl: urls.queryStringUrl,
|
|
273
|
+
authHeaderValue: urls.authHeaderValue,
|
|
274
|
+
hasRuntimeToken: urls.hasRuntimeToken,
|
|
275
|
+
oauthCallbackId: urls.oauthCallbackId,
|
|
276
|
+
oauthCallbackUrl: urls.oauthCallbackUrl,
|
|
277
|
+
oauthCallbackCreatedAt: urls.oauthCallbackCreatedAt,
|
|
278
|
+
oauthCallbackRotatedAt: urls.oauthCallbackRotatedAt,
|
|
279
|
+
oauthCallbackLastUsedAt: urls.oauthCallbackLastUsedAt,
|
|
280
|
+
authNote:
|
|
281
|
+
"All hooks use WEBHOOK_TOKEN. Use Authorization: Bearer <token> or x-openclaw-token header.",
|
|
282
|
+
},
|
|
283
|
+
restartRequired: snapshot.restartRequired,
|
|
284
|
+
syncWarning,
|
|
285
|
+
});
|
|
286
|
+
} catch (err) {
|
|
287
|
+
const status = String(err.message || "").includes("not found") ? 404 : 400;
|
|
288
|
+
return res.status(status).json({ ok: false, error: err.message });
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
212
292
|
app.post("/api/webhooks/:name/oauth-callback", (req, res) => {
|
|
213
293
|
try {
|
|
214
294
|
const name = validateWebhookName(req.params.name);
|
package/lib/server/webhooks.js
CHANGED
|
@@ -412,6 +412,51 @@ const createWebhook = ({
|
|
|
412
412
|
return getWebhookDetail({ fs, constants, name: webhookName });
|
|
413
413
|
};
|
|
414
414
|
|
|
415
|
+
const updateWebhookDestination = ({ fs, constants, name, destination = null }) => {
|
|
416
|
+
const webhookName = validateWebhookName(name);
|
|
417
|
+
const normalizedDestination = normalizeDestination(destination);
|
|
418
|
+
const { cfg, configPath } = readConfig({ fs, constants });
|
|
419
|
+
if (isManagedWebhook({ cfg, name: webhookName })) {
|
|
420
|
+
throw new Error(
|
|
421
|
+
`Webhook "${webhookName}" is managed and cannot be updated manually`,
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
const mappings = ensureHooksRoot(cfg);
|
|
425
|
+
const normalizedModulesChanged = normalizeMappingTransformModules(mappings);
|
|
426
|
+
const index = findMappingIndexByName(mappings, webhookName);
|
|
427
|
+
if (index === -1) {
|
|
428
|
+
throw new Error("Webhook not found");
|
|
429
|
+
}
|
|
430
|
+
const current = mappings[index] || {};
|
|
431
|
+
const agentId = resolveWebhookAgentId({
|
|
432
|
+
cfg,
|
|
433
|
+
requestedAgentId:
|
|
434
|
+
String(normalizedDestination?.agentId || "").trim() ||
|
|
435
|
+
String(current?.agentId || "").trim(),
|
|
436
|
+
});
|
|
437
|
+
const next = {
|
|
438
|
+
...current,
|
|
439
|
+
deliver: true,
|
|
440
|
+
channel:
|
|
441
|
+
String(normalizedDestination?.channel || "").trim() ||
|
|
442
|
+
"last",
|
|
443
|
+
agentId,
|
|
444
|
+
};
|
|
445
|
+
if (String(normalizedDestination?.to || "").trim()) {
|
|
446
|
+
next.to = String(normalizedDestination.to).trim();
|
|
447
|
+
} else {
|
|
448
|
+
delete next.to;
|
|
449
|
+
}
|
|
450
|
+
const changed = JSON.stringify(current) !== JSON.stringify(next);
|
|
451
|
+
if (changed) {
|
|
452
|
+
mappings[index] = next;
|
|
453
|
+
}
|
|
454
|
+
if (changed || normalizedModulesChanged) {
|
|
455
|
+
writeConfig({ fs, configPath, cfg });
|
|
456
|
+
}
|
|
457
|
+
return getWebhookDetail({ fs, constants, name: webhookName });
|
|
458
|
+
};
|
|
459
|
+
|
|
415
460
|
const deleteWebhook = ({ fs, constants, name, deleteTransformDir = false }) => {
|
|
416
461
|
const webhookName = validateWebhookName(name);
|
|
417
462
|
const { cfg, configPath } = readConfig({ fs, constants });
|
|
@@ -457,6 +502,7 @@ module.exports = {
|
|
|
457
502
|
listWebhooks,
|
|
458
503
|
getWebhookDetail,
|
|
459
504
|
createWebhook,
|
|
505
|
+
updateWebhookDestination,
|
|
460
506
|
deleteWebhook,
|
|
461
507
|
validateWebhookName,
|
|
462
508
|
getTransformRelativePath,
|