@chrysb/alphaclaw 0.2.3 → 0.3.0
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/bin/alphaclaw.js +79 -0
- package/lib/public/css/shell.css +57 -2
- package/lib/public/css/theme.css +184 -0
- package/lib/public/js/app.js +330 -89
- package/lib/public/js/components/action-button.js +92 -0
- package/lib/public/js/components/channels.js +16 -7
- package/lib/public/js/components/confirm-dialog.js +25 -19
- package/lib/public/js/components/credentials-modal.js +32 -23
- package/lib/public/js/components/device-pairings.js +15 -2
- package/lib/public/js/components/envars.js +22 -65
- package/lib/public/js/components/features.js +1 -1
- package/lib/public/js/components/gateway.js +139 -32
- package/lib/public/js/components/global-restart-banner.js +31 -0
- package/lib/public/js/components/google.js +9 -9
- package/lib/public/js/components/icons.js +19 -0
- package/lib/public/js/components/info-tooltip.js +18 -0
- package/lib/public/js/components/loading-spinner.js +32 -0
- package/lib/public/js/components/modal-shell.js +42 -0
- package/lib/public/js/components/models.js +34 -29
- package/lib/public/js/components/onboarding/welcome-form-step.js +45 -32
- package/lib/public/js/components/onboarding/welcome-pairing-step.js +2 -2
- package/lib/public/js/components/onboarding/welcome-setup-step.js +7 -24
- package/lib/public/js/components/page-header.js +13 -0
- package/lib/public/js/components/pairings.js +15 -2
- package/lib/public/js/components/providers.js +216 -142
- package/lib/public/js/components/scope-picker.js +1 -1
- package/lib/public/js/components/secret-input.js +1 -0
- package/lib/public/js/components/telegram-workspace.js +37 -49
- package/lib/public/js/components/toast.js +34 -5
- package/lib/public/js/components/toggle-switch.js +25 -0
- package/lib/public/js/components/update-action-button.js +13 -53
- package/lib/public/js/components/watchdog-tab.js +312 -0
- package/lib/public/js/components/webhooks.js +981 -0
- package/lib/public/js/components/welcome.js +2 -1
- package/lib/public/js/lib/api.js +102 -1
- package/lib/public/js/lib/model-config.js +0 -5
- package/lib/server/alphaclaw-version.js +5 -3
- package/lib/server/constants.js +33 -0
- package/lib/server/discord-api.js +48 -0
- package/lib/server/gateway.js +64 -4
- package/lib/server/log-writer.js +102 -0
- package/lib/server/onboarding/github.js +21 -1
- package/lib/server/openclaw-version.js +2 -6
- package/lib/server/restart-required-state.js +86 -0
- package/lib/server/routes/auth.js +9 -4
- package/lib/server/routes/proxy.js +12 -14
- package/lib/server/routes/system.js +61 -15
- package/lib/server/routes/telegram.js +17 -48
- package/lib/server/routes/watchdog.js +68 -0
- package/lib/server/routes/webhooks.js +214 -0
- package/lib/server/telegram-api.js +11 -0
- package/lib/server/watchdog-db.js +148 -0
- package/lib/server/watchdog-notify.js +93 -0
- package/lib/server/watchdog.js +585 -0
- package/lib/server/webhook-middleware.js +195 -0
- package/lib/server/webhooks-db.js +265 -0
- package/lib/server/webhooks.js +238 -0
- package/lib/server.js +119 -4
- package/lib/setup/core-prompts/AGENTS.md +84 -0
- package/lib/setup/core-prompts/TOOLS.md +13 -0
- package/lib/setup/core-prompts/UI-DRY-OPPORTUNITIES.md +50 -0
- package/lib/setup/gitignore +2 -0
- package/package.json +2 -1
|
@@ -1,26 +1,24 @@
|
|
|
1
|
-
const registerProxyRoutes = ({
|
|
1
|
+
const registerProxyRoutes = ({
|
|
2
|
+
app,
|
|
3
|
+
proxy,
|
|
4
|
+
SETUP_API_PREFIXES,
|
|
5
|
+
requireAuth,
|
|
6
|
+
webhookMiddleware,
|
|
7
|
+
}) => {
|
|
2
8
|
app.all("/openclaw", requireAuth, (req, res) => {
|
|
3
9
|
req.url = "/";
|
|
4
10
|
proxy.web(req, res);
|
|
5
11
|
});
|
|
6
|
-
app.all("/openclaw/*
|
|
12
|
+
app.all("/openclaw/*", requireAuth, (req, res) => {
|
|
7
13
|
req.url = req.url.replace(/^\/openclaw/, "");
|
|
8
14
|
proxy.web(req, res);
|
|
9
15
|
});
|
|
10
|
-
app.all("/assets/*
|
|
16
|
+
app.all("/assets/*", requireAuth, (req, res) => proxy.web(req, res));
|
|
11
17
|
|
|
12
|
-
app.all("/
|
|
13
|
-
|
|
14
|
-
req.headers.authorization = `Bearer ${req.query.token}`;
|
|
15
|
-
delete req.query.token;
|
|
16
|
-
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
17
|
-
url.searchParams.delete("token");
|
|
18
|
-
req.url = url.pathname + url.search;
|
|
19
|
-
}
|
|
20
|
-
proxy.web(req, res);
|
|
21
|
-
});
|
|
18
|
+
app.all("/hooks/*", webhookMiddleware);
|
|
19
|
+
app.all("/webhook/*", webhookMiddleware);
|
|
22
20
|
|
|
23
|
-
app.all("/api/*
|
|
21
|
+
app.all("/api/*", (req, res) => {
|
|
24
22
|
if (SETUP_API_PREFIXES.some((p) => req.path.startsWith(p))) return;
|
|
25
23
|
proxy.web(req, res);
|
|
26
24
|
});
|
|
@@ -15,7 +15,9 @@ const registerSystemRoutes = ({
|
|
|
15
15
|
alphaclawVersionService,
|
|
16
16
|
clawCmd,
|
|
17
17
|
restartGateway,
|
|
18
|
+
onExpectedGatewayRestart,
|
|
18
19
|
OPENCLAW_DIR,
|
|
20
|
+
restartRequiredState,
|
|
19
21
|
}) => {
|
|
20
22
|
let envRestartPending = false;
|
|
21
23
|
const kEnvVarsReservedForUserInput = new Set([
|
|
@@ -30,8 +32,7 @@ const registerSystemRoutes = ({
|
|
|
30
32
|
new Set([...kSystemVars, ...kEnvVarsReservedForUserInput]),
|
|
31
33
|
);
|
|
32
34
|
const isReservedUserEnvVar = (key) =>
|
|
33
|
-
kSystemVars.has(key)
|
|
34
|
-
|| kEnvVarsReservedForUserInput.has(key);
|
|
35
|
+
kSystemVars.has(key) || kEnvVarsReservedForUserInput.has(key);
|
|
35
36
|
const kSystemCronPath = "/etc/cron.d/openclaw-hourly-sync";
|
|
36
37
|
const kSystemCronConfigPath = `${OPENCLAW_DIR}/cron/system-sync.json`;
|
|
37
38
|
const kSystemCronScriptPath = `${OPENCLAW_DIR}/hourly-git-sync.sh`;
|
|
@@ -69,11 +70,18 @@ const registerSystemRoutes = ({
|
|
|
69
70
|
};
|
|
70
71
|
const applySystemCronConfig = (nextConfig) => {
|
|
71
72
|
fs.mkdirSync(`${OPENCLAW_DIR}/cron`, { recursive: true });
|
|
72
|
-
fs.writeFileSync(
|
|
73
|
+
fs.writeFileSync(
|
|
74
|
+
kSystemCronConfigPath,
|
|
75
|
+
JSON.stringify(nextConfig, null, 2),
|
|
76
|
+
);
|
|
73
77
|
if (nextConfig.enabled) {
|
|
74
|
-
fs.writeFileSync(
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
fs.writeFileSync(
|
|
79
|
+
kSystemCronPath,
|
|
80
|
+
buildSystemCronContent(nextConfig.schedule),
|
|
81
|
+
{
|
|
82
|
+
mode: 0o644,
|
|
83
|
+
},
|
|
84
|
+
);
|
|
77
85
|
} else {
|
|
78
86
|
fs.rmSync(kSystemCronPath, { force: true });
|
|
79
87
|
}
|
|
@@ -140,7 +148,9 @@ const registerSystemRoutes = ({
|
|
|
140
148
|
}
|
|
141
149
|
|
|
142
150
|
const filtered = vars.filter((v) => !isReservedUserEnvVar(v.key));
|
|
143
|
-
const existingLockedVars = readEnvFile().filter((v) =>
|
|
151
|
+
const existingLockedVars = readEnvFile().filter((v) =>
|
|
152
|
+
isReservedUserEnvVar(v.key),
|
|
153
|
+
);
|
|
144
154
|
const nextEnvVars = [...filtered, ...existingLockedVars];
|
|
145
155
|
syncChannelConfig(nextEnvVars, "remove");
|
|
146
156
|
writeEnvFile(nextEnvVars);
|
|
@@ -149,7 +159,9 @@ const registerSystemRoutes = ({
|
|
|
149
159
|
envRestartPending = true;
|
|
150
160
|
}
|
|
151
161
|
const restartRequired = envRestartPending && isOnboarded();
|
|
152
|
-
console.log(
|
|
162
|
+
console.log(
|
|
163
|
+
`[alphaclaw] Env vars saved (${nextEnvVars.length} vars, changed=${changed})`,
|
|
164
|
+
);
|
|
153
165
|
syncChannelConfig(nextEnvVars, "add");
|
|
154
166
|
|
|
155
167
|
res.json({ ok: true, changed, restartRequired });
|
|
@@ -161,7 +173,11 @@ const registerSystemRoutes = ({
|
|
|
161
173
|
const repo = process.env.GITHUB_WORKSPACE_REPO || "";
|
|
162
174
|
const openclawVersion = openclawVersionService.readOpenclawVersion();
|
|
163
175
|
res.json({
|
|
164
|
-
gateway: running
|
|
176
|
+
gateway: running
|
|
177
|
+
? "running"
|
|
178
|
+
: configExists
|
|
179
|
+
? "starting"
|
|
180
|
+
: "not_onboarded",
|
|
165
181
|
configExists,
|
|
166
182
|
channels: getChannelStatus(),
|
|
167
183
|
repo,
|
|
@@ -178,10 +194,14 @@ const registerSystemRoutes = ({
|
|
|
178
194
|
const current = readSystemCronConfig();
|
|
179
195
|
const { enabled, schedule } = req.body || {};
|
|
180
196
|
if (enabled !== undefined && typeof enabled !== "boolean") {
|
|
181
|
-
return res
|
|
197
|
+
return res
|
|
198
|
+
.status(400)
|
|
199
|
+
.json({ ok: false, error: "enabled must be a boolean" });
|
|
182
200
|
}
|
|
183
201
|
if (schedule !== undefined && !isValidCronSchedule(schedule)) {
|
|
184
|
-
return res
|
|
202
|
+
return res
|
|
203
|
+
.status(400)
|
|
204
|
+
.json({ ok: false, error: "schedule must be a 5-field cron string" });
|
|
185
205
|
}
|
|
186
206
|
const nextConfig = {
|
|
187
207
|
enabled: typeof enabled === "boolean" ? enabled : current.enabled,
|
|
@@ -246,13 +266,39 @@ const registerSystemRoutes = ({
|
|
|
246
266
|
res.json({ ok: true, url: "/openclaw" });
|
|
247
267
|
});
|
|
248
268
|
|
|
249
|
-
app.
|
|
269
|
+
app.get("/api/restart-status", async (req, res) => {
|
|
270
|
+
try {
|
|
271
|
+
const snapshot = await restartRequiredState.getSnapshot();
|
|
272
|
+
res.json({
|
|
273
|
+
ok: true,
|
|
274
|
+
restartRequired: snapshot.restartRequired || envRestartPending,
|
|
275
|
+
restartInProgress: snapshot.restartInProgress,
|
|
276
|
+
gatewayRunning: snapshot.gatewayRunning,
|
|
277
|
+
});
|
|
278
|
+
} catch (err) {
|
|
279
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
app.post("/api/gateway/restart", async (req, res) => {
|
|
250
284
|
if (!isOnboarded()) {
|
|
251
285
|
return res.status(400).json({ ok: false, error: "Not onboarded" });
|
|
252
286
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
287
|
+
restartRequiredState.markRestartInProgress();
|
|
288
|
+
try {
|
|
289
|
+
if (typeof onExpectedGatewayRestart === "function") {
|
|
290
|
+
onExpectedGatewayRestart();
|
|
291
|
+
}
|
|
292
|
+
restartGateway();
|
|
293
|
+
envRestartPending = false;
|
|
294
|
+
restartRequiredState.clearRequired();
|
|
295
|
+
restartRequiredState.markRestartComplete();
|
|
296
|
+
const snapshot = await restartRequiredState.getSnapshot();
|
|
297
|
+
res.json({ ok: true, restartRequired: snapshot.restartRequired });
|
|
298
|
+
} catch (err) {
|
|
299
|
+
restartRequiredState.markRestartComplete();
|
|
300
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
301
|
+
}
|
|
256
302
|
});
|
|
257
303
|
};
|
|
258
304
|
|
|
@@ -4,25 +4,6 @@ const { isDebugEnabled } = require("../helpers");
|
|
|
4
4
|
const topicRegistry = require("../topic-registry");
|
|
5
5
|
const { syncConfigForTelegram } = require("../telegram-workspace");
|
|
6
6
|
|
|
7
|
-
const getRequestBody = (req) => (req.body && typeof req.body === "object" ? req.body : {});
|
|
8
|
-
const getRequestQuery = (req) => (req.query && typeof req.query === "object" ? req.query : {});
|
|
9
|
-
const parseJsonString = (value) => {
|
|
10
|
-
if (typeof value !== "string" || !value.trim()) return null;
|
|
11
|
-
try {
|
|
12
|
-
return JSON.parse(value);
|
|
13
|
-
} catch {
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
|
-
const getRequestPayload = (req) => {
|
|
18
|
-
const body = getRequestBody(req);
|
|
19
|
-
const query = getRequestQuery(req);
|
|
20
|
-
const payloadFromQuery = parseJsonString(query.payload);
|
|
21
|
-
if (payloadFromQuery && typeof payloadFromQuery === "object" && !Array.isArray(payloadFromQuery)) {
|
|
22
|
-
return { ...payloadFromQuery, ...body };
|
|
23
|
-
}
|
|
24
|
-
return body;
|
|
25
|
-
};
|
|
26
7
|
const parseBooleanValue = (value, fallbackValue = false) => {
|
|
27
8
|
if (typeof value === "boolean") return value;
|
|
28
9
|
if (typeof value === "number") return value !== 0;
|
|
@@ -34,9 +15,8 @@ const parseBooleanValue = (value, fallbackValue = false) => {
|
|
|
34
15
|
return fallbackValue;
|
|
35
16
|
};
|
|
36
17
|
const resolveGroupId = (req) => {
|
|
37
|
-
const body =
|
|
38
|
-
const
|
|
39
|
-
const rawGroupId = body.groupId ?? body.chatId ?? query.groupId ?? query.chatId;
|
|
18
|
+
const body = req.body || {};
|
|
19
|
+
const rawGroupId = body.groupId ?? body.chatId;
|
|
40
20
|
return rawGroupId == null ? "" : String(rawGroupId).trim();
|
|
41
21
|
};
|
|
42
22
|
const resolveAllowUserId = async ({ telegramApi, groupId, preferredUserId }) => {
|
|
@@ -121,12 +101,11 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
|
|
|
121
101
|
// Create a topic via Telegram API + add to registry
|
|
122
102
|
app.post("/api/telegram/groups/:groupId/topics", async (req, res) => {
|
|
123
103
|
const { groupId } = req.params;
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
const rawIconColor = payload.iconColor ?? query.iconColor;
|
|
104
|
+
const body = req.body || {};
|
|
105
|
+
const name = String(body.name ?? "").trim();
|
|
106
|
+
const rawIconColor = body.iconColor;
|
|
128
107
|
const systemInstructions = String(
|
|
129
|
-
|
|
108
|
+
body.systemInstructions ?? body.systemPrompt ?? "",
|
|
130
109
|
).trim();
|
|
131
110
|
const iconColorValue = rawIconColor == null ? null : Number.parseInt(String(rawIconColor), 10);
|
|
132
111
|
const iconColor = Number.isFinite(iconColorValue) ? iconColorValue : undefined;
|
|
@@ -160,14 +139,8 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
|
|
|
160
139
|
// Bulk-create topics
|
|
161
140
|
app.post("/api/telegram/groups/:groupId/topics/bulk", async (req, res) => {
|
|
162
141
|
const { groupId } = req.params;
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
const queryTopics = parseJsonString(query.topics);
|
|
166
|
-
const topics = Array.isArray(payload.topics)
|
|
167
|
-
? payload.topics
|
|
168
|
-
: Array.isArray(queryTopics)
|
|
169
|
-
? queryTopics
|
|
170
|
-
: [];
|
|
142
|
+
const body = req.body || {};
|
|
143
|
+
const topics = Array.isArray(body.topics) ? body.topics : [];
|
|
171
144
|
if (!Array.isArray(topics) || topics.length === 0) {
|
|
172
145
|
return res.status(400).json({ ok: false, error: "topics array is required" });
|
|
173
146
|
}
|
|
@@ -247,15 +220,12 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
|
|
|
247
220
|
// Rename a topic
|
|
248
221
|
app.put("/api/telegram/groups/:groupId/topics/:topicId", async (req, res) => {
|
|
249
222
|
const { groupId, topicId } = req.params;
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|| Object.prototype.hasOwnProperty.call(payload, "systemPrompt")
|
|
255
|
-
|| Object.prototype.hasOwnProperty.call(query, "systemInstructions")
|
|
256
|
-
|| Object.prototype.hasOwnProperty.call(query, "systemPrompt");
|
|
223
|
+
const body = req.body || {};
|
|
224
|
+
const name = String(body.name ?? "").trim();
|
|
225
|
+
const hasSystemInstructions = Object.prototype.hasOwnProperty.call(body, "systemInstructions")
|
|
226
|
+
|| Object.prototype.hasOwnProperty.call(body, "systemPrompt");
|
|
257
227
|
const systemInstructions = String(
|
|
258
|
-
|
|
228
|
+
body.systemInstructions ?? body.systemPrompt ?? "",
|
|
259
229
|
).trim();
|
|
260
230
|
if (!name) return res.status(400).json({ ok: false, error: "name is required" });
|
|
261
231
|
try {
|
|
@@ -302,11 +272,10 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
|
|
|
302
272
|
// Configure openclaw.json for a group
|
|
303
273
|
app.post("/api/telegram/groups/:groupId/configure", async (req, res) => {
|
|
304
274
|
const { groupId } = req.params;
|
|
305
|
-
const
|
|
306
|
-
const
|
|
307
|
-
const
|
|
308
|
-
const
|
|
309
|
-
const requireMention = parseBooleanValue(payload.requireMention ?? query.requireMention, false);
|
|
275
|
+
const body = req.body || {};
|
|
276
|
+
const userId = body.userId ?? "";
|
|
277
|
+
const groupName = body.groupName ?? "";
|
|
278
|
+
const requireMention = parseBooleanValue(body.requireMention, false);
|
|
310
279
|
try {
|
|
311
280
|
const resolvedUserId = await resolveAllowUserId({
|
|
312
281
|
telegramApi,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const registerWatchdogRoutes = ({
|
|
2
|
+
app,
|
|
3
|
+
requireAuth,
|
|
4
|
+
watchdog,
|
|
5
|
+
getRecentEvents,
|
|
6
|
+
readLogTail,
|
|
7
|
+
}) => {
|
|
8
|
+
app.get("/api/watchdog/status", requireAuth, (req, res) => {
|
|
9
|
+
try {
|
|
10
|
+
const status = watchdog.getStatus();
|
|
11
|
+
res.json({ ok: true, status });
|
|
12
|
+
} catch (err) {
|
|
13
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
app.get("/api/watchdog/events", requireAuth, (req, res) => {
|
|
18
|
+
try {
|
|
19
|
+
const limit = Number.parseInt(String(req.query.limit || "20"), 10) || 20;
|
|
20
|
+
const includeRoutine =
|
|
21
|
+
String(req.query.includeRoutine || "").trim() === "1" ||
|
|
22
|
+
String(req.query.includeRoutine || "").trim().toLowerCase() === "true";
|
|
23
|
+
const events = getRecentEvents({ limit, includeRoutine });
|
|
24
|
+
res.json({ ok: true, events });
|
|
25
|
+
} catch (err) {
|
|
26
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
app.get("/api/watchdog/logs", requireAuth, (req, res) => {
|
|
31
|
+
try {
|
|
32
|
+
const tail = Number.parseInt(String(req.query.tail || "65536"), 10) || 65536;
|
|
33
|
+
const logs = readLogTail(tail);
|
|
34
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
35
|
+
res.status(200).send(logs);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
app.post("/api/watchdog/repair", requireAuth, async (req, res) => {
|
|
42
|
+
try {
|
|
43
|
+
const result = await watchdog.triggerRepair();
|
|
44
|
+
res.json({ ok: !!result?.ok, result });
|
|
45
|
+
} catch (err) {
|
|
46
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
app.get("/api/watchdog/settings", requireAuth, (req, res) => {
|
|
51
|
+
try {
|
|
52
|
+
res.json({ ok: true, settings: watchdog.getSettings() });
|
|
53
|
+
} catch (err) {
|
|
54
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
app.put("/api/watchdog/settings", requireAuth, (req, res) => {
|
|
59
|
+
try {
|
|
60
|
+
const settings = watchdog.updateSettings(req.body || {});
|
|
61
|
+
res.json({ ok: true, settings });
|
|
62
|
+
} catch (err) {
|
|
63
|
+
res.status(400).json({ ok: false, error: err.message });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
module.exports = { registerWatchdogRoutes };
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
const {
|
|
2
|
+
listWebhooks,
|
|
3
|
+
getWebhookDetail,
|
|
4
|
+
createWebhook,
|
|
5
|
+
deleteWebhook,
|
|
6
|
+
validateWebhookName,
|
|
7
|
+
} = require("../webhooks");
|
|
8
|
+
|
|
9
|
+
const isFiniteInteger = (value) => Number.isFinite(value) && Number.isInteger(value);
|
|
10
|
+
const parseBooleanFlag = (value) => {
|
|
11
|
+
const normalized = String(value == null ? "" : value).trim().toLowerCase();
|
|
12
|
+
return ["1", "true", "yes", "on"].includes(normalized);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const buildHealth = ({ totalCount, errorCount }) => {
|
|
16
|
+
if (!totalCount || totalCount <= 0) return "green";
|
|
17
|
+
if (!errorCount || errorCount <= 0) return "green";
|
|
18
|
+
if (errorCount >= totalCount) return "red";
|
|
19
|
+
return "yellow";
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const mapSummaryByHook = (summaries) => {
|
|
23
|
+
const byHook = new Map();
|
|
24
|
+
for (const summary of summaries || []) byHook.set(summary.hookName, summary);
|
|
25
|
+
return byHook;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const mergeWebhookAndSummary = ({ webhook, summary }) => {
|
|
29
|
+
const totalCount = Number(summary?.totalCount || 0);
|
|
30
|
+
const errorCount = Number(summary?.errorCount || 0);
|
|
31
|
+
const successCount = Number(summary?.successCount || 0);
|
|
32
|
+
return {
|
|
33
|
+
...webhook,
|
|
34
|
+
lastReceived: summary?.lastReceived || null,
|
|
35
|
+
totalCount,
|
|
36
|
+
successCount,
|
|
37
|
+
errorCount,
|
|
38
|
+
health: buildHealth({ totalCount, errorCount }),
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const normalizeStatusFilter = (rawStatus) => {
|
|
43
|
+
const status = String(rawStatus || "all").trim().toLowerCase();
|
|
44
|
+
if (["all", "success", "error"].includes(status)) return status;
|
|
45
|
+
return "all";
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const buildWebhookUrls = ({ baseUrl, name }) => {
|
|
49
|
+
const fullUrl = `${baseUrl}/hooks/${name}`;
|
|
50
|
+
const token = String(process.env.WEBHOOK_TOKEN || "").trim();
|
|
51
|
+
const queryStringUrl = token
|
|
52
|
+
? `${fullUrl}?token=${encodeURIComponent(token)}`
|
|
53
|
+
: `${fullUrl}?token=<WEBHOOK_TOKEN>`;
|
|
54
|
+
const authHeaderValue = token
|
|
55
|
+
? `Authorization: Bearer ${token}`
|
|
56
|
+
: "Authorization: Bearer <WEBHOOK_TOKEN>";
|
|
57
|
+
return { fullUrl, queryStringUrl, authHeaderValue, hasRuntimeToken: !!token };
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const registerWebhookRoutes = ({
|
|
61
|
+
app,
|
|
62
|
+
fs,
|
|
63
|
+
constants,
|
|
64
|
+
getBaseUrl,
|
|
65
|
+
webhooksDb,
|
|
66
|
+
shellCmd,
|
|
67
|
+
restartRequiredState,
|
|
68
|
+
}) => {
|
|
69
|
+
const fallbackRestartState = {
|
|
70
|
+
markRequired: () => {},
|
|
71
|
+
getSnapshot: async () => ({ restartRequired: false }),
|
|
72
|
+
};
|
|
73
|
+
const resolvedRestartState = restartRequiredState || fallbackRestartState;
|
|
74
|
+
const { markRequired: markRestartRequired, getSnapshot: getRestartSnapshot } = resolvedRestartState;
|
|
75
|
+
const runWebhookGitSync = async (action, name) => {
|
|
76
|
+
if (typeof shellCmd !== "function") return null;
|
|
77
|
+
const safeName = String(name || "").trim();
|
|
78
|
+
const message = `webhooks: ${action} ${safeName}`.replace(/"/g, "");
|
|
79
|
+
try {
|
|
80
|
+
await shellCmd(`alphaclaw git-sync -m "${message}"`, {
|
|
81
|
+
timeout: 30000,
|
|
82
|
+
});
|
|
83
|
+
return null;
|
|
84
|
+
} catch (err) {
|
|
85
|
+
return err?.message || "alphaclaw git-sync failed";
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
app.get("/api/webhooks", (req, res) => {
|
|
90
|
+
try {
|
|
91
|
+
const hooks = listWebhooks({ fs, constants });
|
|
92
|
+
const summaries = webhooksDb.getHookSummaries();
|
|
93
|
+
const summaryByHook = mapSummaryByHook(summaries);
|
|
94
|
+
const webhooks = hooks.map((webhook) =>
|
|
95
|
+
mergeWebhookAndSummary({
|
|
96
|
+
webhook,
|
|
97
|
+
summary: summaryByHook.get(webhook.name),
|
|
98
|
+
}));
|
|
99
|
+
res.json({ ok: true, webhooks });
|
|
100
|
+
} catch (err) {
|
|
101
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
app.get("/api/webhooks/:name", (req, res) => {
|
|
106
|
+
try {
|
|
107
|
+
const name = validateWebhookName(req.params.name);
|
|
108
|
+
const detail = getWebhookDetail({ fs, constants, name });
|
|
109
|
+
if (!detail) return res.status(404).json({ ok: false, error: "Webhook not found" });
|
|
110
|
+
const summary = webhooksDb.getHookSummaries().find((item) => item.hookName === name);
|
|
111
|
+
const merged = mergeWebhookAndSummary({ webhook: detail, summary });
|
|
112
|
+
const baseUrl = getBaseUrl(req);
|
|
113
|
+
const urls = buildWebhookUrls({ baseUrl, name });
|
|
114
|
+
return res.json({
|
|
115
|
+
ok: true,
|
|
116
|
+
webhook: {
|
|
117
|
+
...merged,
|
|
118
|
+
fullUrl: urls.fullUrl,
|
|
119
|
+
queryStringUrl: urls.queryStringUrl,
|
|
120
|
+
authHeaderValue: urls.authHeaderValue,
|
|
121
|
+
hasRuntimeToken: urls.hasRuntimeToken,
|
|
122
|
+
authNote:
|
|
123
|
+
"All hooks use WEBHOOK_TOKEN. Use Authorization: Bearer <token> or x-openclaw-token header.",
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
} catch (err) {
|
|
127
|
+
return res.status(400).json({ ok: false, error: err.message });
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
app.post("/api/webhooks", async (req, res) => {
|
|
132
|
+
try {
|
|
133
|
+
const { name: rawName } = req.body || {};
|
|
134
|
+
const name = validateWebhookName(rawName);
|
|
135
|
+
const webhook = createWebhook({ fs, constants, name });
|
|
136
|
+
const baseUrl = getBaseUrl(req);
|
|
137
|
+
const urls = buildWebhookUrls({ baseUrl, name });
|
|
138
|
+
const syncWarning = await runWebhookGitSync("create", name);
|
|
139
|
+
markRestartRequired("webhooks");
|
|
140
|
+
const snapshot = await getRestartSnapshot();
|
|
141
|
+
return res.status(201).json({
|
|
142
|
+
ok: true,
|
|
143
|
+
webhook: {
|
|
144
|
+
...webhook,
|
|
145
|
+
fullUrl: urls.fullUrl,
|
|
146
|
+
queryStringUrl: urls.queryStringUrl,
|
|
147
|
+
authHeaderValue: urls.authHeaderValue,
|
|
148
|
+
hasRuntimeToken: urls.hasRuntimeToken,
|
|
149
|
+
},
|
|
150
|
+
restartRequired: snapshot.restartRequired,
|
|
151
|
+
syncWarning,
|
|
152
|
+
});
|
|
153
|
+
} catch (err) {
|
|
154
|
+
const status = String(err.message || "").includes("already exists") ? 409 : 400;
|
|
155
|
+
return res.status(status).json({ ok: false, error: err.message });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
app.delete("/api/webhooks/:name", async (req, res) => {
|
|
160
|
+
try {
|
|
161
|
+
const name = validateWebhookName(req.params.name);
|
|
162
|
+
const deleteTransformDir = parseBooleanFlag(req?.body?.deleteTransformDir);
|
|
163
|
+
const deletion = deleteWebhook({ fs, constants, name, deleteTransformDir });
|
|
164
|
+
if (!deletion?.removed) return res.status(404).json({ ok: false, error: "Webhook not found" });
|
|
165
|
+
const deletedRequestCount = webhooksDb.deleteRequestsByHook(name);
|
|
166
|
+
const syncWarning = await runWebhookGitSync("delete", name);
|
|
167
|
+
markRestartRequired("webhooks");
|
|
168
|
+
const snapshot = await getRestartSnapshot();
|
|
169
|
+
return res.json({
|
|
170
|
+
ok: true,
|
|
171
|
+
restartRequired: snapshot.restartRequired,
|
|
172
|
+
syncWarning,
|
|
173
|
+
deletedRequestCount,
|
|
174
|
+
deletedTransformDir: !!deletion.deletedTransformDir,
|
|
175
|
+
});
|
|
176
|
+
} catch (err) {
|
|
177
|
+
return res.status(400).json({ ok: false, error: err.message });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
app.get("/api/webhooks/:name/requests", (req, res) => {
|
|
182
|
+
try {
|
|
183
|
+
const name = validateWebhookName(req.params.name);
|
|
184
|
+
const limit = Number.parseInt(String(req.query.limit || 50), 10);
|
|
185
|
+
const offset = Number.parseInt(String(req.query.offset || 0), 10);
|
|
186
|
+
const status = normalizeStatusFilter(req.query.status);
|
|
187
|
+
const hasBadPaging = !isFiniteInteger(limit) || limit <= 0 || !isFiniteInteger(offset) || offset < 0;
|
|
188
|
+
if (hasBadPaging) {
|
|
189
|
+
return res.status(400).json({ ok: false, error: "Invalid limit/offset" });
|
|
190
|
+
}
|
|
191
|
+
const requests = webhooksDb.getRequests(name, { limit, offset, status });
|
|
192
|
+
return res.json({ ok: true, requests });
|
|
193
|
+
} catch (err) {
|
|
194
|
+
return res.status(400).json({ ok: false, error: err.message });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
app.get("/api/webhooks/:name/requests/:id", (req, res) => {
|
|
199
|
+
try {
|
|
200
|
+
const name = validateWebhookName(req.params.name);
|
|
201
|
+
const requestId = Number.parseInt(String(req.params.id || 0), 10);
|
|
202
|
+
if (!isFiniteInteger(requestId) || requestId <= 0) {
|
|
203
|
+
return res.status(400).json({ ok: false, error: "Invalid request id" });
|
|
204
|
+
}
|
|
205
|
+
const request = webhooksDb.getRequestById(name, requestId);
|
|
206
|
+
if (!request) return res.status(404).json({ ok: false, error: "Request not found" });
|
|
207
|
+
return res.json({ ok: true, request });
|
|
208
|
+
} catch (err) {
|
|
209
|
+
return res.status(400).json({ ok: false, error: err.message });
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
module.exports = { registerWebhookRoutes };
|
|
@@ -51,6 +51,16 @@ const createTelegramApi = (getToken) => {
|
|
|
51
51
|
...(opts.iconCustomEmojiId && { icon_custom_emoji_id: opts.iconCustomEmojiId }),
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
const sendMessage = (chatId, text, opts = {}) =>
|
|
55
|
+
call("sendMessage", {
|
|
56
|
+
chat_id: chatId,
|
|
57
|
+
text: String(text || ""),
|
|
58
|
+
...(opts.parseMode && { parse_mode: opts.parseMode }),
|
|
59
|
+
...(opts.disableWebPagePreview && {
|
|
60
|
+
disable_web_page_preview: !!opts.disableWebPagePreview,
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
|
|
54
64
|
return {
|
|
55
65
|
getMe,
|
|
56
66
|
getChat,
|
|
@@ -59,6 +69,7 @@ const createTelegramApi = (getToken) => {
|
|
|
59
69
|
createForumTopic,
|
|
60
70
|
deleteForumTopic,
|
|
61
71
|
editForumTopic,
|
|
72
|
+
sendMessage,
|
|
62
73
|
};
|
|
63
74
|
};
|
|
64
75
|
|