@chrysb/alphaclaw 0.4.6-beta.6 → 0.4.6-beta.7
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/agent-send-modal.js +142 -0
- package/lib/public/js/components/doctor/fix-card-modal.js +15 -89
- package/lib/public/js/components/gateway.js +1 -0
- package/lib/public/js/components/google/gmail-setup-wizard.js +28 -4
- package/lib/public/js/components/webhooks.js +103 -2
- package/lib/server/constants.js +6 -0
- package/lib/server/gateway.js +41 -1
- package/lib/server/gmail-watch.js +6 -3
- package/lib/server/onboarding/index.js +14 -1
- package/lib/server/routes/system.js +26 -12
- package/lib/server/watchdog.js +2 -2
- package/lib/server.js +0 -1
- package/package.json +1 -1
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useEffect, useState } from "https://esm.sh/preact/hooks";
|
|
3
|
+
import htm from "https://esm.sh/htm";
|
|
4
|
+
import { ModalShell } from "./modal-shell.js";
|
|
5
|
+
import { ActionButton } from "./action-button.js";
|
|
6
|
+
import { PageHeader } from "./page-header.js";
|
|
7
|
+
import { CloseIcon } from "./icons.js";
|
|
8
|
+
import { useAgentSessions } from "../hooks/useAgentSessions.js";
|
|
9
|
+
|
|
10
|
+
const html = htm.bind(h);
|
|
11
|
+
|
|
12
|
+
export const AgentSendModal = ({
|
|
13
|
+
visible = false,
|
|
14
|
+
title = "Send to agent",
|
|
15
|
+
messageLabel = "Message",
|
|
16
|
+
messageRows = 8,
|
|
17
|
+
initialMessage = "",
|
|
18
|
+
resetKey = "",
|
|
19
|
+
submitLabel = "Send message",
|
|
20
|
+
loadingLabel = "Sending...",
|
|
21
|
+
cancelLabel = "Cancel",
|
|
22
|
+
onClose = () => {},
|
|
23
|
+
onSubmit = async () => true,
|
|
24
|
+
sessionFilter = undefined,
|
|
25
|
+
}) => {
|
|
26
|
+
const {
|
|
27
|
+
sessions,
|
|
28
|
+
selectedSessionKey,
|
|
29
|
+
setSelectedSessionKey,
|
|
30
|
+
selectedSession,
|
|
31
|
+
loading: loadingSessions,
|
|
32
|
+
error: loadError,
|
|
33
|
+
} = useAgentSessions({ enabled: visible, filter: sessionFilter });
|
|
34
|
+
const [messageText, setMessageText] = useState("");
|
|
35
|
+
const [sending, setSending] = useState(false);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!visible) return;
|
|
39
|
+
setMessageText(String(initialMessage || ""));
|
|
40
|
+
}, [visible, initialMessage, resetKey]);
|
|
41
|
+
|
|
42
|
+
const handleSend = async () => {
|
|
43
|
+
if (!selectedSession || sending) return;
|
|
44
|
+
const trimmedMessage = String(messageText || "").trim();
|
|
45
|
+
if (!trimmedMessage) return;
|
|
46
|
+
setSending(true);
|
|
47
|
+
try {
|
|
48
|
+
const shouldClose = await onSubmit({
|
|
49
|
+
selectedSession,
|
|
50
|
+
selectedSessionKey,
|
|
51
|
+
message: trimmedMessage,
|
|
52
|
+
});
|
|
53
|
+
if (shouldClose !== false) {
|
|
54
|
+
onClose();
|
|
55
|
+
}
|
|
56
|
+
} finally {
|
|
57
|
+
setSending(false);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return html`
|
|
62
|
+
<${ModalShell}
|
|
63
|
+
visible=${visible}
|
|
64
|
+
onClose=${() => {
|
|
65
|
+
if (sending) return;
|
|
66
|
+
onClose();
|
|
67
|
+
}}
|
|
68
|
+
panelClassName="bg-modal border border-border rounded-xl p-5 max-w-lg w-full space-y-4"
|
|
69
|
+
>
|
|
70
|
+
<${PageHeader}
|
|
71
|
+
title=${title}
|
|
72
|
+
actions=${html`
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
onclick=${() => {
|
|
76
|
+
if (sending) return;
|
|
77
|
+
onClose();
|
|
78
|
+
}}
|
|
79
|
+
class="h-8 w-8 inline-flex items-center justify-center rounded-lg ac-btn-secondary"
|
|
80
|
+
aria-label="Close modal"
|
|
81
|
+
>
|
|
82
|
+
<${CloseIcon} className="w-3.5 h-3.5 text-gray-300" />
|
|
83
|
+
</button>
|
|
84
|
+
`}
|
|
85
|
+
/>
|
|
86
|
+
<div class="space-y-2">
|
|
87
|
+
<label class="text-xs text-gray-500">Send to session</label>
|
|
88
|
+
<select
|
|
89
|
+
value=${selectedSessionKey}
|
|
90
|
+
onChange=${(event) =>
|
|
91
|
+
setSelectedSessionKey(String(event.currentTarget?.value || ""))}
|
|
92
|
+
disabled=${loadingSessions || sending}
|
|
93
|
+
class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-xs text-gray-200 focus:border-gray-500"
|
|
94
|
+
>
|
|
95
|
+
${sessions.length === 0
|
|
96
|
+
? html`<option value="">No sessions available</option>`
|
|
97
|
+
: null}
|
|
98
|
+
${sessions.map(
|
|
99
|
+
(sessionRow) => html`
|
|
100
|
+
<option value=${String(sessionRow?.key || "")}>
|
|
101
|
+
${String(sessionRow?.label || sessionRow?.key || "Session")}
|
|
102
|
+
</option>
|
|
103
|
+
`,
|
|
104
|
+
)}
|
|
105
|
+
</select>
|
|
106
|
+
${loadingSessions
|
|
107
|
+
? html`<div class="text-xs text-gray-500">Loading sessions...</div>`
|
|
108
|
+
: null}
|
|
109
|
+
${loadError ? html`<div class="text-xs text-red-400">${loadError}</div>` : null}
|
|
110
|
+
</div>
|
|
111
|
+
<div class="space-y-2">
|
|
112
|
+
<label class="text-xs text-gray-500">${messageLabel}</label>
|
|
113
|
+
<textarea
|
|
114
|
+
value=${messageText}
|
|
115
|
+
onInput=${(event) =>
|
|
116
|
+
setMessageText(String(event.currentTarget?.value || ""))}
|
|
117
|
+
disabled=${sending}
|
|
118
|
+
rows=${messageRows}
|
|
119
|
+
class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-xs text-gray-200 focus:border-gray-500 font-mono leading-5"
|
|
120
|
+
></textarea>
|
|
121
|
+
</div>
|
|
122
|
+
<div class="flex items-center justify-end gap-2">
|
|
123
|
+
<${ActionButton}
|
|
124
|
+
onClick=${onClose}
|
|
125
|
+
disabled=${sending}
|
|
126
|
+
tone="secondary"
|
|
127
|
+
size="md"
|
|
128
|
+
idleLabel=${cancelLabel}
|
|
129
|
+
/>
|
|
130
|
+
<${ActionButton}
|
|
131
|
+
onClick=${handleSend}
|
|
132
|
+
disabled=${!selectedSession || loadingSessions || !!loadError || !String(messageText || "").trim()}
|
|
133
|
+
loading=${sending}
|
|
134
|
+
tone="primary"
|
|
135
|
+
size="md"
|
|
136
|
+
idleLabel=${submitLabel}
|
|
137
|
+
loadingLabel=${loadingLabel}
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
</${ModalShell}>
|
|
141
|
+
`;
|
|
142
|
+
};
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
|
-
import { useEffect, useState } from "https://esm.sh/preact/hooks";
|
|
3
2
|
import htm from "https://esm.sh/htm";
|
|
4
|
-
import { ModalShell } from "../modal-shell.js";
|
|
5
|
-
import { ActionButton } from "../action-button.js";
|
|
6
3
|
import { sendDoctorCardFix, updateDoctorCardStatus } from "../../lib/api.js";
|
|
7
4
|
import { showToast } from "../toast.js";
|
|
8
|
-
import {
|
|
5
|
+
import { AgentSendModal } from "../agent-send-modal.js";
|
|
9
6
|
|
|
10
7
|
const html = htm.bind(h);
|
|
11
8
|
|
|
@@ -15,33 +12,15 @@ export const DoctorFixCardModal = ({
|
|
|
15
12
|
onClose = () => {},
|
|
16
13
|
onComplete = () => {},
|
|
17
14
|
}) => {
|
|
18
|
-
const {
|
|
19
|
-
|
|
20
|
-
selectedSessionKey,
|
|
21
|
-
setSelectedSessionKey,
|
|
22
|
-
selectedSession,
|
|
23
|
-
loading: loadingSessions,
|
|
24
|
-
error: loadError,
|
|
25
|
-
} = useAgentSessions({ enabled: visible });
|
|
26
|
-
|
|
27
|
-
const [sending, setSending] = useState(false);
|
|
28
|
-
const [promptText, setPromptText] = useState("");
|
|
29
|
-
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
if (!visible) return;
|
|
32
|
-
setPromptText(String(card?.fixPrompt || ""));
|
|
33
|
-
}, [visible, card?.fixPrompt, card?.id]);
|
|
34
|
-
|
|
35
|
-
const handleSend = async () => {
|
|
36
|
-
if (!card?.id || sending) return;
|
|
15
|
+
const handleSend = async ({ selectedSession, message }) => {
|
|
16
|
+
if (!card?.id) return false;
|
|
37
17
|
try {
|
|
38
|
-
setSending(true);
|
|
39
18
|
await sendDoctorCardFix({
|
|
40
19
|
cardId: card.id,
|
|
41
20
|
sessionId: selectedSession?.sessionId || "",
|
|
42
21
|
replyChannel: selectedSession?.replyChannel || "",
|
|
43
22
|
replyTo: selectedSession?.replyTo || "",
|
|
44
|
-
prompt:
|
|
23
|
+
prompt: message,
|
|
45
24
|
});
|
|
46
25
|
try {
|
|
47
26
|
await updateDoctorCardStatus({ cardId: card.id, status: "fixed" });
|
|
@@ -57,77 +36,24 @@ export const DoctorFixCardModal = ({
|
|
|
57
36
|
);
|
|
58
37
|
}
|
|
59
38
|
await onComplete();
|
|
60
|
-
|
|
39
|
+
return true;
|
|
61
40
|
} catch (error) {
|
|
62
41
|
showToast(error.message || "Could not send Doctor fix request", "error");
|
|
63
|
-
|
|
64
|
-
setSending(false);
|
|
42
|
+
return false;
|
|
65
43
|
}
|
|
66
44
|
};
|
|
67
45
|
|
|
68
46
|
return html`
|
|
69
|
-
<${
|
|
47
|
+
<${AgentSendModal}
|
|
70
48
|
visible=${visible}
|
|
49
|
+
title="Ask agent to fix"
|
|
50
|
+
messageLabel="Instructions"
|
|
51
|
+
initialMessage=${String(card?.fixPrompt || "")}
|
|
52
|
+
resetKey=${String(card?.id || "")}
|
|
53
|
+
submitLabel="Send fix request"
|
|
54
|
+
loadingLabel="Sending..."
|
|
71
55
|
onClose=${onClose}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
<div class="space-y-1">
|
|
75
|
-
<h2 class="text-base font-semibold">Ask agent to fix</h2>
|
|
76
|
-
<p class="text-xs text-gray-400">
|
|
77
|
-
Send this Doctor finding to one of your agent sessions as a focused fix request.
|
|
78
|
-
</p>
|
|
79
|
-
</div>
|
|
80
|
-
<div class="space-y-2">
|
|
81
|
-
<label class="text-xs text-gray-500">Send to session</label>
|
|
82
|
-
<select
|
|
83
|
-
value=${selectedSessionKey}
|
|
84
|
-
onChange=${(event) => setSelectedSessionKey(String(event.currentTarget?.value || ""))}
|
|
85
|
-
disabled=${loadingSessions || sending}
|
|
86
|
-
class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-xs text-gray-200 focus:border-gray-500"
|
|
87
|
-
>
|
|
88
|
-
${sessions.map(
|
|
89
|
-
(sessionRow) => html`
|
|
90
|
-
<option value=${String(sessionRow?.key || "")}>
|
|
91
|
-
${String(sessionRow?.label || sessionRow?.key || "Session")}
|
|
92
|
-
</option>
|
|
93
|
-
`,
|
|
94
|
-
)}
|
|
95
|
-
</select>
|
|
96
|
-
${
|
|
97
|
-
loadingSessions
|
|
98
|
-
? html`<div class="text-xs text-gray-500">Loading sessions...</div>`
|
|
99
|
-
: null
|
|
100
|
-
}
|
|
101
|
-
${loadError ? html`<div class="text-xs text-red-400">${loadError}</div>` : null}
|
|
102
|
-
</div>
|
|
103
|
-
<div class="space-y-2">
|
|
104
|
-
<label class="text-xs text-gray-500">Instructions</label>
|
|
105
|
-
<textarea
|
|
106
|
-
value=${promptText}
|
|
107
|
-
onInput=${(event) => setPromptText(String(event.currentTarget?.value || ""))}
|
|
108
|
-
disabled=${sending}
|
|
109
|
-
rows="8"
|
|
110
|
-
class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-xs text-gray-200 focus:border-gray-500 font-mono leading-5"
|
|
111
|
-
></textarea>
|
|
112
|
-
</div>
|
|
113
|
-
<div class="flex items-center justify-end gap-2">
|
|
114
|
-
<${ActionButton}
|
|
115
|
-
onClick=${onClose}
|
|
116
|
-
disabled=${sending}
|
|
117
|
-
tone="secondary"
|
|
118
|
-
size="md"
|
|
119
|
-
idleLabel="Cancel"
|
|
120
|
-
/>
|
|
121
|
-
<${ActionButton}
|
|
122
|
-
onClick=${handleSend}
|
|
123
|
-
disabled=${!selectedSession || loadingSessions || !!loadError || !String(promptText || "").trim()}
|
|
124
|
-
loading=${sending}
|
|
125
|
-
tone="primary"
|
|
126
|
-
size="md"
|
|
127
|
-
idleLabel="Send fix request"
|
|
128
|
-
loadingLabel="Sending..."
|
|
129
|
-
/>
|
|
130
|
-
</div>
|
|
131
|
-
</${ModalShell}>
|
|
56
|
+
onSubmit=${handleSend}
|
|
57
|
+
/>
|
|
132
58
|
`;
|
|
133
59
|
};
|
|
@@ -326,6 +326,7 @@ export const Gateway = ({
|
|
|
326
326
|
const showInspectButton = watchdogHealth === "degraded" && !!onOpenWatchdog;
|
|
327
327
|
const showRepairButton =
|
|
328
328
|
isRepairInProgress ||
|
|
329
|
+
(watchdogStatus?.health === "degraded" && !onOpenWatchdog) ||
|
|
329
330
|
watchdogStatus?.lifecycle === "crash_loop" ||
|
|
330
331
|
watchdogStatus?.health === "unhealthy" ||
|
|
331
332
|
watchdogStatus?.health === "crashed";
|
|
@@ -81,6 +81,7 @@ export const GmailSetupWizard = ({
|
|
|
81
81
|
}) => {
|
|
82
82
|
const [step, setStep] = useState(0);
|
|
83
83
|
const [projectIdInput, setProjectIdInput] = useState("");
|
|
84
|
+
const [editingProjectId, setEditingProjectId] = useState(false);
|
|
84
85
|
const [localError, setLocalError] = useState("");
|
|
85
86
|
const [projectIdResolved, setProjectIdResolved] = useState(false);
|
|
86
87
|
const [watchEnabled, setWatchEnabled] = useState(false);
|
|
@@ -100,6 +101,7 @@ export const GmailSetupWizard = ({
|
|
|
100
101
|
setStep(0);
|
|
101
102
|
setLocalError("");
|
|
102
103
|
setProjectIdInput("");
|
|
104
|
+
setEditingProjectId(false);
|
|
103
105
|
setProjectIdResolved(false);
|
|
104
106
|
setWatchEnabled(false);
|
|
105
107
|
setSendingToAgent(false);
|
|
@@ -110,10 +112,11 @@ export const GmailSetupWizard = ({
|
|
|
110
112
|
const hasProjectIdFromConfig = Boolean(
|
|
111
113
|
String(clientConfig?.projectId || "").trim() || commands,
|
|
112
114
|
);
|
|
113
|
-
const needsProjectId =
|
|
115
|
+
const needsProjectId =
|
|
116
|
+
editingProjectId || (!hasProjectIdFromConfig && !projectIdResolved);
|
|
114
117
|
const detectedProjectId =
|
|
115
|
-
String(clientConfig?.projectId || "").trim() ||
|
|
116
118
|
String(projectIdInput || "").trim() ||
|
|
119
|
+
String(clientConfig?.projectId || "").trim() ||
|
|
117
120
|
"<project-id>";
|
|
118
121
|
const client =
|
|
119
122
|
String(account?.client || clientConfig?.client || "default").trim() ||
|
|
@@ -135,6 +138,13 @@ export const GmailSetupWizard = ({
|
|
|
135
138
|
showToast("Could not copy text", "error");
|
|
136
139
|
}, []);
|
|
137
140
|
|
|
141
|
+
const handleChangeProjectId = useCallback(() => {
|
|
142
|
+
setLocalError("");
|
|
143
|
+
setProjectIdInput(String(clientConfig?.projectId || "").trim());
|
|
144
|
+
setProjectIdResolved(false);
|
|
145
|
+
setEditingProjectId(true);
|
|
146
|
+
}, [clientConfig?.projectId]);
|
|
147
|
+
|
|
138
148
|
const handleFinish = async () => {
|
|
139
149
|
try {
|
|
140
150
|
setLocalError("");
|
|
@@ -159,6 +169,7 @@ export const GmailSetupWizard = ({
|
|
|
159
169
|
client,
|
|
160
170
|
projectId: String(projectIdInput || "").trim(),
|
|
161
171
|
});
|
|
172
|
+
setEditingProjectId(false);
|
|
162
173
|
setProjectIdResolved(true);
|
|
163
174
|
} catch (err) {
|
|
164
175
|
setLocalError(err.message || "Could not save project id");
|
|
@@ -230,7 +241,9 @@ export const GmailSetupWizard = ({
|
|
|
230
241
|
<div
|
|
231
242
|
class="rounded-lg border border-border bg-black/20 p-3 space-y-2"
|
|
232
243
|
>
|
|
233
|
-
<div class="text-sm">
|
|
244
|
+
<div class="text-sm">
|
|
245
|
+
${editingProjectId ? "Change project ID" : "Project ID required"}
|
|
246
|
+
</div>
|
|
234
247
|
<div class="text-xs text-gray-500">
|
|
235
248
|
Find it in the${" "}
|
|
236
249
|
<a
|
|
@@ -257,6 +270,9 @@ export const GmailSetupWizard = ({
|
|
|
257
270
|
!needsProjectId && step === 0
|
|
258
271
|
? html`
|
|
259
272
|
<div class="space-y-1">
|
|
273
|
+
<div class="text-xs text-gray-500">
|
|
274
|
+
Using project <code>${detectedProjectId}</code>.
|
|
275
|
+
</div>
|
|
260
276
|
<div class="text-xs text-gray-500">
|
|
261
277
|
If <code>gcloud</code> is not installed on your computer,
|
|
262
278
|
follow the official install guide:${" "}
|
|
@@ -372,7 +388,15 @@ export const GmailSetupWizard = ({
|
|
|
372
388
|
}
|
|
373
389
|
<div class="grid grid-cols-2 gap-2 pt-2">
|
|
374
390
|
${step === 0
|
|
375
|
-
? html
|
|
391
|
+
? html`${!needsProjectId
|
|
392
|
+
? html`<button
|
|
393
|
+
type="button"
|
|
394
|
+
onclick=${handleChangeProjectId}
|
|
395
|
+
class="justify-self-start text-xs px-2 py-1 rounded-lg ac-btn-ghost"
|
|
396
|
+
>
|
|
397
|
+
Change project ID
|
|
398
|
+
</button>`
|
|
399
|
+
: html`<div></div>`}`
|
|
376
400
|
: html`<${ActionButton}
|
|
377
401
|
onClick=${() => setStep((prev) => Math.max(prev - 1, 0))}
|
|
378
402
|
disabled=${saving}
|
|
@@ -6,8 +6,10 @@ import {
|
|
|
6
6
|
createWebhook,
|
|
7
7
|
deleteWebhook,
|
|
8
8
|
fetchWebhookDetail,
|
|
9
|
+
fetchWebhookRequest,
|
|
9
10
|
fetchWebhookRequests,
|
|
10
11
|
fetchWebhooks,
|
|
12
|
+
sendAgentMessage,
|
|
11
13
|
} from "../lib/api.js";
|
|
12
14
|
import {
|
|
13
15
|
formatLocaleDateTime,
|
|
@@ -17,6 +19,7 @@ import { showToast } from "./toast.js";
|
|
|
17
19
|
import { PageHeader } from "./page-header.js";
|
|
18
20
|
import { ConfirmDialog } from "./confirm-dialog.js";
|
|
19
21
|
import { ActionButton } from "./action-button.js";
|
|
22
|
+
import { AgentSendModal } from "./agent-send-modal.js";
|
|
20
23
|
import { ModalShell } from "./modal-shell.js";
|
|
21
24
|
import { Badge } from "./badge.js";
|
|
22
25
|
import { CloseIcon } from "./icons.js";
|
|
@@ -82,6 +85,40 @@ const jsonPretty = (value) => {
|
|
|
82
85
|
}
|
|
83
86
|
};
|
|
84
87
|
|
|
88
|
+
const buildWebhookDebugMessage = ({
|
|
89
|
+
hookName = "",
|
|
90
|
+
webhook = null,
|
|
91
|
+
request = null,
|
|
92
|
+
}) => {
|
|
93
|
+
const hookPath =
|
|
94
|
+
String(webhook?.path || "").trim() ||
|
|
95
|
+
(hookName ? `/hooks/${hookName}` : "/hooks/unknown");
|
|
96
|
+
const gatewayStatus =
|
|
97
|
+
request?.gatewayStatus == null ? "n/a" : String(request.gatewayStatus);
|
|
98
|
+
return [
|
|
99
|
+
"Investigate this failed webhook request and share findings before fixing anything.",
|
|
100
|
+
"Reply with your diagnosis first, including the likely root cause, any relevant risks, and what you would change if I approve a fix.",
|
|
101
|
+
"",
|
|
102
|
+
`Webhook: ${hookPath}`,
|
|
103
|
+
`Request ID: ${String(request?.id || "unknown")}`,
|
|
104
|
+
`Time: ${String(request?.createdAt || "unknown")}`,
|
|
105
|
+
`Method: ${String(request?.method || "unknown")}`,
|
|
106
|
+
`Source IP: ${String(request?.sourceIp || "unknown")}`,
|
|
107
|
+
`Gateway status: ${gatewayStatus}`,
|
|
108
|
+
`Transform path: ${String(webhook?.transformPath || "unknown")}`,
|
|
109
|
+
`Payload truncated: ${request?.payloadTruncated ? "yes" : "no"}`,
|
|
110
|
+
"",
|
|
111
|
+
"Headers:",
|
|
112
|
+
jsonPretty(request?.headers),
|
|
113
|
+
"",
|
|
114
|
+
"Payload:",
|
|
115
|
+
jsonPretty(request?.payload),
|
|
116
|
+
"",
|
|
117
|
+
"Gateway response:",
|
|
118
|
+
jsonPretty(request?.gatewayBody),
|
|
119
|
+
].join("\n");
|
|
120
|
+
};
|
|
121
|
+
|
|
85
122
|
const CreateWebhookModal = ({
|
|
86
123
|
visible,
|
|
87
124
|
name,
|
|
@@ -197,6 +234,8 @@ export const Webhooks = ({
|
|
|
197
234
|
const [expandedRows, setExpandedRows] = useState(() => new Set());
|
|
198
235
|
const [sendingTestWebhook, setSendingTestWebhook] = useState(false);
|
|
199
236
|
const [replayingRequestId, setReplayingRequestId] = useState(null);
|
|
237
|
+
const [debugLoadingRequestId, setDebugLoadingRequestId] = useState(null);
|
|
238
|
+
const [debugRequest, setDebugRequest] = useState(null);
|
|
200
239
|
|
|
201
240
|
const listPoll = usePolling(fetchWebhooks, 15000);
|
|
202
241
|
const webhooks = listPoll.data?.webhooks || [];
|
|
@@ -496,6 +535,31 @@ export const Webhooks = ({
|
|
|
496
535
|
}, []);
|
|
497
536
|
|
|
498
537
|
const isListLoading = !listPoll.data && !listPoll.error;
|
|
538
|
+
const debugAgentMessage = useMemo(
|
|
539
|
+
() =>
|
|
540
|
+
buildWebhookDebugMessage({
|
|
541
|
+
hookName: selectedHookName,
|
|
542
|
+
webhook: selectedWebhook,
|
|
543
|
+
request: debugRequest,
|
|
544
|
+
}),
|
|
545
|
+
[debugRequest, selectedHookName, selectedWebhook],
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
const handleAskAgentToDebug = useCallback(
|
|
549
|
+
async (item) => {
|
|
550
|
+
if (!selectedHookName || !item?.id || debugLoadingRequestId === item.id) return;
|
|
551
|
+
try {
|
|
552
|
+
setDebugLoadingRequestId(item.id);
|
|
553
|
+
const data = await fetchWebhookRequest(selectedHookName, item.id);
|
|
554
|
+
setDebugRequest(data?.request || item);
|
|
555
|
+
} catch (err) {
|
|
556
|
+
showToast(err.message || "Could not load webhook request details", "error");
|
|
557
|
+
} finally {
|
|
558
|
+
setDebugLoadingRequestId(null);
|
|
559
|
+
}
|
|
560
|
+
},
|
|
561
|
+
[debugLoadingRequestId, selectedHookName],
|
|
562
|
+
);
|
|
499
563
|
|
|
500
564
|
return html`
|
|
501
565
|
<div class="space-y-4">
|
|
@@ -841,7 +905,7 @@ export const Webhooks = ({
|
|
|
841
905
|
>
|
|
842
906
|
${jsonPretty(item.headers)}</pre
|
|
843
907
|
>
|
|
844
|
-
<div class="mt-2 flex justify-start">
|
|
908
|
+
<div class="mt-2 flex justify-start gap-2">
|
|
845
909
|
<button
|
|
846
910
|
class="h-7 text-xs px-2.5 rounded-lg ac-btn-secondary"
|
|
847
911
|
onclick=${() =>
|
|
@@ -905,7 +969,7 @@ ${jsonPretty(item.payload)}</pre
|
|
|
905
969
|
>
|
|
906
970
|
${jsonPretty(item.gatewayBody)}</pre
|
|
907
971
|
>
|
|
908
|
-
<div class="mt-2 flex justify-start">
|
|
972
|
+
<div class="mt-2 flex justify-start gap-2">
|
|
909
973
|
<button
|
|
910
974
|
class="h-7 text-xs px-2.5 rounded-lg ac-btn-secondary"
|
|
911
975
|
onclick=${() =>
|
|
@@ -916,6 +980,19 @@ ${jsonPretty(item.gatewayBody)}</pre
|
|
|
916
980
|
>
|
|
917
981
|
Copy
|
|
918
982
|
</button>
|
|
983
|
+
${item.status === "error"
|
|
984
|
+
? html`<${ActionButton}
|
|
985
|
+
onClick=${() =>
|
|
986
|
+
handleAskAgentToDebug(item)}
|
|
987
|
+
loading=${debugLoadingRequestId ===
|
|
988
|
+
item.id}
|
|
989
|
+
tone="primary"
|
|
990
|
+
size="sm"
|
|
991
|
+
idleLabel="Ask agent to debug"
|
|
992
|
+
loadingLabel="Loading..."
|
|
993
|
+
className="h-7 px-2.5"
|
|
994
|
+
/>`
|
|
995
|
+
: null}
|
|
919
996
|
</div>
|
|
920
997
|
</div>
|
|
921
998
|
</div>
|
|
@@ -1058,6 +1135,30 @@ ${jsonPretty(item.gatewayBody)}</pre
|
|
|
1058
1135
|
}}
|
|
1059
1136
|
onConfirm=${handleDeleteConfirmed}
|
|
1060
1137
|
/>
|
|
1138
|
+
<${AgentSendModal}
|
|
1139
|
+
visible=${!!debugRequest}
|
|
1140
|
+
title="Ask agent to debug"
|
|
1141
|
+
messageLabel="Debug request"
|
|
1142
|
+
messageRows=${12}
|
|
1143
|
+
initialMessage=${debugAgentMessage}
|
|
1144
|
+
resetKey=${String(debugRequest?.id || "")}
|
|
1145
|
+
submitLabel="Send debug request"
|
|
1146
|
+
loadingLabel="Sending..."
|
|
1147
|
+
onClose=${() => setDebugRequest(null)}
|
|
1148
|
+
onSubmit=${async ({ selectedSessionKey, message }) => {
|
|
1149
|
+
try {
|
|
1150
|
+
await sendAgentMessage({
|
|
1151
|
+
message,
|
|
1152
|
+
sessionKey: selectedSessionKey,
|
|
1153
|
+
});
|
|
1154
|
+
showToast("Debug request sent to agent", "success");
|
|
1155
|
+
return true;
|
|
1156
|
+
} catch (err) {
|
|
1157
|
+
showToast(err.message || "Could not send debug request", "error");
|
|
1158
|
+
return false;
|
|
1159
|
+
}
|
|
1160
|
+
}}
|
|
1161
|
+
/>
|
|
1061
1162
|
</div>
|
|
1062
1163
|
`;
|
|
1063
1164
|
};
|
package/lib/server/constants.js
CHANGED
|
@@ -6,6 +6,7 @@ const { parsePositiveInt } = require("./utils/number");
|
|
|
6
6
|
// Portable root directory: --root-dir flag sets ALPHACLAW_ROOT_DIR before require
|
|
7
7
|
const kRootDir =
|
|
8
8
|
process.env.ALPHACLAW_ROOT_DIR || path.join(os.homedir(), ".alphaclaw");
|
|
9
|
+
const ALPHACLAW_DIR = kRootDir;
|
|
9
10
|
const kPackageRoot = path.resolve(__dirname, "..");
|
|
10
11
|
const kNpmPackageRoot = path.resolve(kPackageRoot, "..");
|
|
11
12
|
const kSetupDir = path.join(kPackageRoot, "setup");
|
|
@@ -18,6 +19,8 @@ const OPENCLAW_DIR = path.join(kRootDir, ".openclaw");
|
|
|
18
19
|
const GATEWAY_TOKEN = process.env.OPENCLAW_GATEWAY_TOKEN || "";
|
|
19
20
|
const ENV_FILE_PATH = path.join(kRootDir, ".env");
|
|
20
21
|
const WORKSPACE_DIR = path.join(OPENCLAW_DIR, "workspace");
|
|
22
|
+
const kOnboardingMarkerPath = path.join(ALPHACLAW_DIR, "onboarded.json");
|
|
23
|
+
const kControlUiSkillPath = path.join(OPENCLAW_DIR, "skills", "control-ui", "SKILL.md");
|
|
21
24
|
const AUTH_PROFILES_PATH = path.join(
|
|
22
25
|
OPENCLAW_DIR,
|
|
23
26
|
"agents",
|
|
@@ -346,6 +349,7 @@ const SETUP_API_PREFIXES = [
|
|
|
346
349
|
];
|
|
347
350
|
|
|
348
351
|
module.exports = {
|
|
352
|
+
ALPHACLAW_DIR,
|
|
349
353
|
kRootDir,
|
|
350
354
|
kPackageRoot,
|
|
351
355
|
kNpmPackageRoot,
|
|
@@ -358,6 +362,8 @@ module.exports = {
|
|
|
358
362
|
GATEWAY_TOKEN,
|
|
359
363
|
ENV_FILE_PATH,
|
|
360
364
|
WORKSPACE_DIR,
|
|
365
|
+
kOnboardingMarkerPath,
|
|
366
|
+
kControlUiSkillPath,
|
|
361
367
|
AUTH_PROFILES_PATH,
|
|
362
368
|
CODEX_PROFILE_ID,
|
|
363
369
|
CODEX_OAUTH_CLIENT_ID,
|
package/lib/server/gateway.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const path = require("path");
|
|
1
2
|
const { spawn, execSync } = require("child_process");
|
|
2
3
|
const fs = require("fs");
|
|
3
4
|
const net = require("net");
|
|
@@ -5,7 +6,9 @@ const {
|
|
|
5
6
|
OPENCLAW_DIR,
|
|
6
7
|
GATEWAY_HOST,
|
|
7
8
|
GATEWAY_PORT,
|
|
9
|
+
kControlUiSkillPath,
|
|
8
10
|
kChannelDefs,
|
|
11
|
+
kOnboardingMarkerPath,
|
|
9
12
|
kRootDir,
|
|
10
13
|
} = require("./constants");
|
|
11
14
|
|
|
@@ -45,7 +48,7 @@ const gatewayEnv = () => ({
|
|
|
45
48
|
XDG_CONFIG_HOME: OPENCLAW_DIR,
|
|
46
49
|
});
|
|
47
50
|
|
|
48
|
-
const
|
|
51
|
+
const hasOnboardingModelConfig = () => {
|
|
49
52
|
const configPath = `${OPENCLAW_DIR}/openclaw.json`;
|
|
50
53
|
if (!fs.existsSync(configPath)) return false;
|
|
51
54
|
try {
|
|
@@ -59,6 +62,43 @@ const isOnboarded = () => {
|
|
|
59
62
|
}
|
|
60
63
|
};
|
|
61
64
|
|
|
65
|
+
const hasLegacyOnboardingArtifacts = () => fs.existsSync(kControlUiSkillPath);
|
|
66
|
+
|
|
67
|
+
const writeOnboardingMarker = (reason) => {
|
|
68
|
+
try {
|
|
69
|
+
fs.mkdirSync(path.dirname(kOnboardingMarkerPath), { recursive: true });
|
|
70
|
+
fs.writeFileSync(
|
|
71
|
+
kOnboardingMarkerPath,
|
|
72
|
+
JSON.stringify(
|
|
73
|
+
{
|
|
74
|
+
onboarded: true,
|
|
75
|
+
reason: String(reason || "unknown"),
|
|
76
|
+
markedAt: new Date().toISOString(),
|
|
77
|
+
},
|
|
78
|
+
null,
|
|
79
|
+
2,
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
return true;
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error(`[alphaclaw] Failed to write onboarding marker: ${err.message}`);
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const isOnboarded = () => {
|
|
90
|
+
if (fs.existsSync(kOnboardingMarkerPath)) return true;
|
|
91
|
+
if (hasOnboardingModelConfig()) {
|
|
92
|
+
writeOnboardingMarker("config_primary_model");
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
if (hasLegacyOnboardingArtifacts()) {
|
|
96
|
+
writeOnboardingMarker("legacy_artifact_backfill");
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
};
|
|
101
|
+
|
|
62
102
|
const isGatewayRunning = () =>
|
|
63
103
|
new Promise((resolve) => {
|
|
64
104
|
const sock = net.createConnection(GATEWAY_PORT, GATEWAY_HOST);
|
|
@@ -70,19 +70,22 @@ const ensureTopicPathForClient = ({
|
|
|
70
70
|
const normalizedClient = String(client || "default").trim() || "default";
|
|
71
71
|
const push = getGmailPushConfig(state);
|
|
72
72
|
const existingTopic = String(push.topics?.[normalizedClient] || "").trim();
|
|
73
|
-
|
|
73
|
+
const requestedProjectId = String(projectIdOverride || "").trim();
|
|
74
|
+
const existingProjectId = parseProjectIdFromTopicPath(existingTopic);
|
|
75
|
+
if (existingTopic && (!requestedProjectId || requestedProjectId === existingProjectId)) {
|
|
74
76
|
return { state, topicPath: existingTopic };
|
|
75
77
|
}
|
|
76
78
|
const credentials = readGoogleCredentials(normalizedClient);
|
|
77
79
|
const projectId =
|
|
78
|
-
|
|
80
|
+
requestedProjectId ||
|
|
79
81
|
String(credentials?.projectId || "").trim();
|
|
80
82
|
if (!projectId) {
|
|
81
83
|
throw new Error(
|
|
82
84
|
`Could not detect GCP project_id for client "${normalizedClient}". Save Google credentials first.`,
|
|
83
85
|
);
|
|
84
86
|
}
|
|
85
|
-
const topicName =
|
|
87
|
+
const topicName =
|
|
88
|
+
parseTopicName(existingTopic) || createTopicNameForClient(normalizedClient);
|
|
86
89
|
const topicPath = `projects/${projectId}/topics/${topicName}`;
|
|
87
90
|
const updated = setGmailPushConfig({
|
|
88
91
|
state,
|
|
@@ -34,7 +34,7 @@ const createOnboardingService = ({
|
|
|
34
34
|
getBaseUrl,
|
|
35
35
|
startGateway,
|
|
36
36
|
}) => {
|
|
37
|
-
const { OPENCLAW_DIR, WORKSPACE_DIR } = constants;
|
|
37
|
+
const { OPENCLAW_DIR, WORKSPACE_DIR, kOnboardingMarkerPath } = constants;
|
|
38
38
|
|
|
39
39
|
const verifyGithubSetup = async ({
|
|
40
40
|
githubRepoInput,
|
|
@@ -160,6 +160,19 @@ const createOnboardingService = ({
|
|
|
160
160
|
|
|
161
161
|
installHourlyGitSyncScript({ fs, openclawDir: OPENCLAW_DIR });
|
|
162
162
|
await installHourlyGitSyncCron({ fs, openclawDir: OPENCLAW_DIR });
|
|
163
|
+
fs.mkdirSync(path.dirname(kOnboardingMarkerPath), { recursive: true });
|
|
164
|
+
fs.writeFileSync(
|
|
165
|
+
kOnboardingMarkerPath,
|
|
166
|
+
JSON.stringify(
|
|
167
|
+
{
|
|
168
|
+
onboarded: true,
|
|
169
|
+
reason: "onboarding_complete",
|
|
170
|
+
markedAt: new Date().toISOString(),
|
|
171
|
+
},
|
|
172
|
+
null,
|
|
173
|
+
2,
|
|
174
|
+
),
|
|
175
|
+
);
|
|
163
176
|
|
|
164
177
|
try {
|
|
165
178
|
await shellCmd(`alphaclaw git-sync -m "initial setup"`, {
|
|
@@ -17,7 +17,6 @@ const registerSystemRoutes = ({
|
|
|
17
17
|
alphaclawVersionService,
|
|
18
18
|
clawCmd,
|
|
19
19
|
restartGateway,
|
|
20
|
-
onExpectedGatewayRestart,
|
|
21
20
|
OPENCLAW_DIR,
|
|
22
21
|
restartRequiredState,
|
|
23
22
|
topicRegistry,
|
|
@@ -59,7 +58,9 @@ const registerSystemRoutes = ({
|
|
|
59
58
|
const parseJsonFromStdout = (stdout) => {
|
|
60
59
|
const raw = String(stdout || "").trim();
|
|
61
60
|
if (!raw) return null;
|
|
62
|
-
const candidateStarts = [raw.indexOf("{"), raw.indexOf("[")].filter(
|
|
61
|
+
const candidateStarts = [raw.indexOf("{"), raw.indexOf("[")].filter(
|
|
62
|
+
(idx) => idx >= 0,
|
|
63
|
+
);
|
|
63
64
|
for (const start of candidateStarts) {
|
|
64
65
|
const candidate = raw.slice(start);
|
|
65
66
|
try {
|
|
@@ -75,7 +76,9 @@ const registerSystemRoutes = ({
|
|
|
75
76
|
if (telegramMatch) {
|
|
76
77
|
return `Telegram ${telegramMatch[1]}`;
|
|
77
78
|
}
|
|
78
|
-
const telegramTopicMatch = key.match(
|
|
79
|
+
const telegramTopicMatch = key.match(
|
|
80
|
+
/:telegram:group:([^:]+):topic:([^:]+)$/,
|
|
81
|
+
);
|
|
79
82
|
if (telegramTopicMatch) {
|
|
80
83
|
const [, groupId, topicId] = telegramTopicMatch;
|
|
81
84
|
let groupEntry = null;
|
|
@@ -83,7 +86,9 @@ const registerSystemRoutes = ({
|
|
|
83
86
|
groupEntry = topicRegistry?.getGroup?.(groupId) || null;
|
|
84
87
|
} catch {}
|
|
85
88
|
const groupName = String(groupEntry?.name || "").trim();
|
|
86
|
-
const topicName = String(
|
|
89
|
+
const topicName = String(
|
|
90
|
+
groupEntry?.topics?.[topicId]?.name || "",
|
|
91
|
+
).trim();
|
|
87
92
|
if (groupName && topicName) return `Telegram ${groupName} · ${topicName}`;
|
|
88
93
|
if (topicName) return `Telegram Topic ${topicName}`;
|
|
89
94
|
return `Telegram Topic ${topicId}`;
|
|
@@ -266,13 +271,19 @@ const registerSystemRoutes = ({
|
|
|
266
271
|
);
|
|
267
272
|
const hiddenKnownVarKeys = new Set(
|
|
268
273
|
kKnownVars
|
|
269
|
-
.filter(
|
|
274
|
+
.filter(
|
|
275
|
+
(def) => !isReservedUserEnvVar(def.key) && !isVisibleInEnvars(def),
|
|
276
|
+
)
|
|
270
277
|
.map((def) => def.key),
|
|
271
278
|
);
|
|
272
279
|
const existingHiddenKnownVars = readEnvFile().filter((v) =>
|
|
273
280
|
hiddenKnownVarKeys.has(v.key),
|
|
274
281
|
);
|
|
275
|
-
const nextEnvVars = [
|
|
282
|
+
const nextEnvVars = [
|
|
283
|
+
...filtered,
|
|
284
|
+
...existingHiddenKnownVars,
|
|
285
|
+
...existingLockedVars,
|
|
286
|
+
];
|
|
276
287
|
syncChannelConfig(nextEnvVars, "remove");
|
|
277
288
|
writeEnvFile(nextEnvVars);
|
|
278
289
|
const changed = reloadEnv();
|
|
@@ -402,12 +413,15 @@ const registerSystemRoutes = ({
|
|
|
402
413
|
let selectedSession = null;
|
|
403
414
|
try {
|
|
404
415
|
const sessions = await listSendableAgentSessions();
|
|
405
|
-
selectedSession =
|
|
416
|
+
selectedSession =
|
|
417
|
+
sessions.find((sessionRow) => sessionRow.key === sessionKey) || null;
|
|
406
418
|
} catch (err) {
|
|
407
419
|
return res.status(502).json({ ok: false, error: err.message });
|
|
408
420
|
}
|
|
409
421
|
if (!selectedSession) {
|
|
410
|
-
return res
|
|
422
|
+
return res
|
|
423
|
+
.status(400)
|
|
424
|
+
.json({ ok: false, error: "Selected session was not found" });
|
|
411
425
|
}
|
|
412
426
|
if (selectedSession.replyChannel && selectedSession.replyTo) {
|
|
413
427
|
command +=
|
|
@@ -421,7 +435,10 @@ const registerSystemRoutes = ({
|
|
|
421
435
|
if (!result.ok) {
|
|
422
436
|
return res
|
|
423
437
|
.status(502)
|
|
424
|
-
.json({
|
|
438
|
+
.json({
|
|
439
|
+
ok: false,
|
|
440
|
+
error: result.stderr || "Could not send message to agent",
|
|
441
|
+
});
|
|
425
442
|
}
|
|
426
443
|
return res.json({ ok: true, stdout: result.stdout || "" });
|
|
427
444
|
});
|
|
@@ -458,9 +475,6 @@ const registerSystemRoutes = ({
|
|
|
458
475
|
}
|
|
459
476
|
restartRequiredState.markRestartInProgress();
|
|
460
477
|
try {
|
|
461
|
-
if (typeof onExpectedGatewayRestart === "function") {
|
|
462
|
-
onExpectedGatewayRestart();
|
|
463
|
-
}
|
|
464
478
|
restartGateway();
|
|
465
479
|
envRestartPending = false;
|
|
466
480
|
restartRequiredState.clearRequired();
|
package/lib/server/watchdog.js
CHANGED
|
@@ -9,7 +9,7 @@ const {
|
|
|
9
9
|
|
|
10
10
|
const kHealthStartupGraceMs = 30 * 1000;
|
|
11
11
|
const kBootstrapHealthCheckMs = 5 * 1000;
|
|
12
|
-
const kExpectedRestartWindowMs =
|
|
12
|
+
const kExpectedRestartWindowMs = 15 * 1000;
|
|
13
13
|
|
|
14
14
|
const isTruthy = (value) =>
|
|
15
15
|
["1", "true", "yes", "on"].includes(String(value || "").trim().toLowerCase());
|
|
@@ -506,7 +506,7 @@ const createWatchdog = ({
|
|
|
506
506
|
const onGatewayExit = ({ code, signal, expectedExit = false, stderrTail = [] } = {}) => {
|
|
507
507
|
const correlationId = createCorrelationId();
|
|
508
508
|
clearDegradedHealthCheckTimer();
|
|
509
|
-
if (expectedExit) {
|
|
509
|
+
if (expectedExit && (code == null || code === 0)) {
|
|
510
510
|
state.lifecycle = "restarting";
|
|
511
511
|
state.health = "unknown";
|
|
512
512
|
state.crashRecoveryActive = false;
|
package/lib/server.js
CHANGED