@chrysb/alphaclaw 0.8.1-beta.2 → 0.8.1-beta.4
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 +69 -7
- package/lib/public/js/components/webhooks/webhook-detail/use-webhook-detail.js +200 -14
- package/lib/public/js/hooks/use-destination-session-selection.js +8 -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,10 +19,15 @@ 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 {
|
|
@@ -30,6 +38,12 @@ export const WebhookDetail = ({
|
|
|
30
38
|
selectedWebhookManaged,
|
|
31
39
|
selectedDeliveryAgentName,
|
|
32
40
|
selectedDeliveryChannel,
|
|
41
|
+
selectableSessions,
|
|
42
|
+
loadingDestinationSessions,
|
|
43
|
+
destinationLoadError,
|
|
44
|
+
destinationSessionKey,
|
|
45
|
+
destinationDirty,
|
|
46
|
+
savingDestination,
|
|
33
47
|
webhookUrl,
|
|
34
48
|
oauthCallbackUrl,
|
|
35
49
|
hasOauthCallback,
|
|
@@ -222,13 +236,60 @@ export const WebhookDetail = ({
|
|
|
222
236
|
`}
|
|
223
237
|
|
|
224
238
|
<div class="bg-black/20 border border-border rounded-lg p-3 space-y-2">
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
+
`}
|
|
232
293
|
</div>
|
|
233
294
|
|
|
234
295
|
<div class="bg-black/20 border border-border rounded-lg p-3 space-y-2">
|
|
@@ -333,6 +394,7 @@ export const WebhookDetail = ({
|
|
|
333
394
|
webhookUrl=${webhookUrl}
|
|
334
395
|
webhookUrlWithQueryToken=${webhookUrlWithQueryToken}
|
|
335
396
|
bearerTokenValue=${bearerTokenValue}
|
|
397
|
+
refreshNonce=${historyRefreshNonce}
|
|
336
398
|
/>
|
|
337
399
|
<${ConfirmDialog}
|
|
338
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";
|
|
14
|
+
import {
|
|
15
|
+
useDestinationSessionSelection,
|
|
16
|
+
} from "../../../hooks/use-destination-session-selection.js";
|
|
8
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,6 +75,7 @@ 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
80
|
const detailCacheKey = useMemo(
|
|
26
81
|
() => `/api/webhooks/${encodeURIComponent(String(selectedHookName || ""))}`,
|
|
@@ -68,10 +123,57 @@ export const useWebhookDetail = ({
|
|
|
68
123
|
formatAgentFallbackName(selectedDeliveryAgentId);
|
|
69
124
|
const selectedDeliveryChannel =
|
|
70
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
|
+
});
|
|
71
154
|
|
|
72
155
|
const webhookUrl = selectedWebhook?.fullUrl || `.../hooks/${selectedHookName}`;
|
|
73
156
|
const oauthCallbackUrl = String(selectedWebhook?.oauthCallbackUrl || "").trim();
|
|
74
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]);
|
|
75
177
|
const webhookUrlWithQueryToken =
|
|
76
178
|
selectedWebhook?.queryStringUrl ||
|
|
77
179
|
`${webhookUrl}${webhookUrl.includes("?") ? "&" : "?"}token=<WEBHOOK_TOKEN>`;
|
|
@@ -133,31 +235,104 @@ export const useWebhookDetail = ({
|
|
|
133
235
|
`curl -X POST "${webhookUrlWithQueryToken}" ` +
|
|
134
236
|
`-H "Content-Type: application/json" ` +
|
|
135
237
|
`-d '${webhookTestPayloadJson}'`;
|
|
238
|
+
const curlCommandOauth = `curl -X GET "${oauthCallbackTestUrl}"`;
|
|
136
239
|
|
|
137
240
|
const effectiveAuthMode = selectedWebhookManaged ? "headers" : authMode;
|
|
138
|
-
const activeCurlCommand =
|
|
139
|
-
|
|
241
|
+
const activeCurlCommand = hasOauthCallback
|
|
242
|
+
? curlCommandOauth
|
|
243
|
+
: effectiveAuthMode === "query"
|
|
244
|
+
? curlCommandQuery
|
|
245
|
+
: curlCommandHeaders;
|
|
140
246
|
|
|
141
247
|
const refreshDetail = useCallback(() => {
|
|
142
248
|
detailFetchState.refresh({ force: true });
|
|
143
249
|
agentsFetchState.refresh({ force: true });
|
|
144
250
|
}, [agentsFetchState.refresh, detailFetchState.refresh]);
|
|
145
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
|
+
]);
|
|
313
|
+
|
|
146
314
|
const handleSendTestWebhook = useCallback(async () => {
|
|
147
315
|
if (!selectedHookName || sendingTestWebhook) return;
|
|
148
316
|
setSendingTestWebhook(true);
|
|
149
|
-
const requestUrl =
|
|
150
|
-
effectiveAuthMode === "query" ? webhookUrlWithQueryToken : webhookUrl;
|
|
151
|
-
const headers = { "Content-Type": "application/json" };
|
|
152
|
-
if (effectiveAuthMode === "headers") {
|
|
153
|
-
headers.Authorization = bearerTokenValue;
|
|
154
|
-
}
|
|
155
317
|
try {
|
|
156
|
-
const response =
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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();
|
|
161
336
|
const bodyText = await response.text();
|
|
162
337
|
let body = null;
|
|
163
338
|
try {
|
|
@@ -184,6 +359,9 @@ export const useWebhookDetail = ({
|
|
|
184
359
|
}, [
|
|
185
360
|
bearerTokenValue,
|
|
186
361
|
effectiveAuthMode,
|
|
362
|
+
hasOauthCallback,
|
|
363
|
+
oauthCallbackTestUrl,
|
|
364
|
+
onTestWebhookSent,
|
|
187
365
|
selectedHookName,
|
|
188
366
|
sendingTestWebhook,
|
|
189
367
|
webhookTestPayloadJson,
|
|
@@ -248,6 +426,12 @@ export const useWebhookDetail = ({
|
|
|
248
426
|
selectedWebhookManaged,
|
|
249
427
|
selectedDeliveryAgentName,
|
|
250
428
|
selectedDeliveryChannel,
|
|
429
|
+
selectableSessions,
|
|
430
|
+
loadingDestinationSessions,
|
|
431
|
+
destinationLoadError,
|
|
432
|
+
destinationSessionKey,
|
|
433
|
+
destinationDirty,
|
|
434
|
+
savingDestination,
|
|
251
435
|
webhookUrl,
|
|
252
436
|
oauthCallbackUrl,
|
|
253
437
|
hasOauthCallback,
|
|
@@ -266,9 +450,11 @@ export const useWebhookDetail = ({
|
|
|
266
450
|
actions: {
|
|
267
451
|
refreshDetail,
|
|
268
452
|
setAuthMode,
|
|
453
|
+
setDestinationSessionKey,
|
|
269
454
|
setShowDeleteConfirm,
|
|
270
455
|
setDeleteTransformDir,
|
|
271
456
|
setShowRotateOauthConfirm,
|
|
457
|
+
handleSaveDestination,
|
|
272
458
|
handleDeleteConfirmed,
|
|
273
459
|
handleRotateOauthCallback,
|
|
274
460
|
handleSendTestWebhook,
|
|
@@ -1,27 +1,13 @@
|
|
|
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
|
+
getSessionRowKey,
|
|
6
|
+
kDestinationSessionFilter,
|
|
7
|
+
} from "../lib/session-keys.js";
|
|
3
8
|
|
|
4
9
|
export const kNoDestinationSessionValue = "__none__";
|
|
5
10
|
|
|
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
11
|
export const useDestinationSessionSelection = ({
|
|
26
12
|
enabled = false,
|
|
27
13
|
resetKey = "",
|
|
@@ -48,10 +34,10 @@ export const useDestinationSessionSelection = ({
|
|
|
48
34
|
const preferredSessionKey = useMemo(() => {
|
|
49
35
|
const matchingPreferredSession = sessions.find(
|
|
50
36
|
(sessionRow) =>
|
|
51
|
-
|
|
37
|
+
getSessionRowKey(sessionRow) === String(selectedSessionKey || "").trim(),
|
|
52
38
|
);
|
|
53
39
|
return String(
|
|
54
|
-
matchingPreferredSession
|
|
40
|
+
getSessionRowKey(matchingPreferredSession) || getSessionRowKey(sessions[0]),
|
|
55
41
|
).trim();
|
|
56
42
|
}, [sessions, selectedSessionKey]);
|
|
57
43
|
|
|
@@ -63,7 +49,7 @@ export const useDestinationSessionSelection = ({
|
|
|
63
49
|
() =>
|
|
64
50
|
sessions.find(
|
|
65
51
|
(sessionRow) =>
|
|
66
|
-
|
|
52
|
+
getSessionRowKey(sessionRow) === String(effectiveSessionKey || "").trim(),
|
|
67
53
|
) || null,
|
|
68
54
|
[effectiveSessionKey, sessions],
|
|
69
55
|
);
|
|
@@ -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,
|