@chrysb/alphaclaw 0.3.5-beta.1 → 0.4.1-beta.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 +1 -31
- package/lib/public/assets/icons/google_icon.svg +8 -0
- package/lib/public/css/explorer.css +53 -0
- package/lib/public/css/shell.css +21 -19
- package/lib/public/css/theme.css +17 -0
- package/lib/public/js/app.js +205 -109
- package/lib/public/js/components/credentials-modal.js +36 -8
- package/lib/public/js/components/file-tree.js +212 -22
- package/lib/public/js/components/file-viewer/editor-surface.js +5 -0
- package/lib/public/js/components/file-viewer/index.js +47 -6
- package/lib/public/js/components/file-viewer/markdown-split-view.js +2 -0
- package/lib/public/js/components/file-viewer/status-banners.js +11 -6
- package/lib/public/js/components/file-viewer/toolbar.js +56 -1
- package/lib/public/js/components/file-viewer/use-editor-selection-restore.js +6 -0
- package/lib/public/js/components/file-viewer/use-file-diff.js +11 -0
- package/lib/public/js/components/file-viewer/use-file-loader.js +12 -2
- package/lib/public/js/components/file-viewer/use-file-viewer.js +142 -15
- package/lib/public/js/components/google/account-row.js +131 -0
- package/lib/public/js/components/google/add-account-modal.js +93 -0
- package/lib/public/js/components/google/gmail-setup-wizard.js +450 -0
- package/lib/public/js/components/google/gmail-watch-toggle.js +81 -0
- package/lib/public/js/components/google/index.js +553 -0
- package/lib/public/js/components/google/use-gmail-watch.js +140 -0
- package/lib/public/js/components/google/use-google-accounts.js +41 -0
- package/lib/public/js/components/icons.js +26 -0
- package/lib/public/js/components/scope-picker.js +1 -1
- package/lib/public/js/components/sidebar-git-panel.js +48 -20
- package/lib/public/js/components/sidebar.js +93 -75
- package/lib/public/js/components/toast.js +11 -7
- package/lib/public/js/components/usage-tab/constants.js +31 -0
- package/lib/public/js/components/usage-tab/formatters.js +24 -0
- package/lib/public/js/components/usage-tab/index.js +72 -0
- package/lib/public/js/components/usage-tab/overview-section.js +147 -0
- package/lib/public/js/components/usage-tab/sessions-section.js +175 -0
- package/lib/public/js/components/usage-tab/use-usage-tab.js +241 -0
- package/lib/public/js/components/webhooks.js +182 -129
- package/lib/public/js/lib/api.js +178 -9
- package/lib/public/js/lib/browse-file-policies.js +29 -11
- package/lib/public/js/lib/format.js +71 -0
- package/lib/public/js/lib/syntax-highlighters/index.js +6 -5
- package/lib/public/shared/browse-file-policies.json +13 -0
- package/lib/server/constants.js +47 -7
- package/lib/server/gmail-push.js +109 -0
- package/lib/server/gmail-serve.js +254 -0
- package/lib/server/gmail-watch.js +725 -0
- package/lib/server/google-state.js +317 -0
- package/lib/server/helpers.js +17 -11
- package/lib/server/internal-files-migration.js +31 -3
- package/lib/server/onboarding/github.js +21 -2
- package/lib/server/onboarding/index.js +1 -3
- package/lib/server/onboarding/openclaw.js +3 -0
- package/lib/server/onboarding/workspace.js +40 -0
- package/lib/server/routes/browse/index.js +90 -2
- package/lib/server/routes/gmail.js +128 -0
- package/lib/server/routes/google.js +433 -213
- package/lib/server/routes/system.js +107 -0
- package/lib/server/routes/usage.js +29 -2
- package/lib/server/routes/webhooks.js +52 -17
- package/lib/server/usage-db.js +283 -15
- package/lib/server/watchdog.js +66 -0
- package/lib/server/webhook-middleware.js +99 -1
- package/lib/server/webhooks.js +214 -65
- package/lib/server.js +27 -0
- package/lib/setup/gitignore +6 -0
- package/lib/setup/hourly-git-sync.sh +29 -2
- package/package.json +1 -1
- package/lib/public/js/components/google.js +0 -228
- package/lib/public/js/components/usage-tab.js +0 -531
package/lib/server/watchdog.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const {
|
|
2
2
|
kWatchdogCheckIntervalMs,
|
|
3
|
+
kWatchdogDegradedCheckIntervalMs,
|
|
4
|
+
kWatchdogStartupFailureThreshold,
|
|
3
5
|
kWatchdogMaxRepairAttempts,
|
|
4
6
|
kWatchdogCrashLoopWindowMs,
|
|
5
7
|
kWatchdogCrashLoopThreshold,
|
|
@@ -64,9 +66,34 @@ const createWatchdog = ({
|
|
|
64
66
|
expectedRestartInProgress: false,
|
|
65
67
|
expectedRestartUntilMs: 0,
|
|
66
68
|
pendingRecoveryNoticeSource: "",
|
|
69
|
+
startupConsecutiveHealthFailures: 0,
|
|
67
70
|
};
|
|
68
71
|
let healthTimer = null;
|
|
69
72
|
let bootstrapHealthTimer = null;
|
|
73
|
+
let degradedHealthTimer = null;
|
|
74
|
+
|
|
75
|
+
const clearDegradedHealthCheckTimer = () => {
|
|
76
|
+
if (!degradedHealthTimer) return;
|
|
77
|
+
clearTimeout(degradedHealthTimer);
|
|
78
|
+
degradedHealthTimer = null;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const scheduleDegradedHealthCheck = () => {
|
|
82
|
+
if (degradedHealthTimer) return;
|
|
83
|
+
if (state.health !== "degraded" || state.lifecycle !== "running") return;
|
|
84
|
+
degradedHealthTimer = setTimeout(async () => {
|
|
85
|
+
degradedHealthTimer = null;
|
|
86
|
+
if (state.health !== "degraded" || state.lifecycle !== "running") return;
|
|
87
|
+
await runHealthCheck({
|
|
88
|
+
source: "degraded_retry",
|
|
89
|
+
allowAutoRepair: false,
|
|
90
|
+
});
|
|
91
|
+
if (state.health === "degraded" && state.lifecycle === "running") {
|
|
92
|
+
scheduleDegradedHealthCheck();
|
|
93
|
+
}
|
|
94
|
+
}, kWatchdogDegradedCheckIntervalMs);
|
|
95
|
+
if (typeof degradedHealthTimer.unref === "function") degradedHealthTimer.unref();
|
|
96
|
+
};
|
|
70
97
|
|
|
71
98
|
const clearExpectedRestartWindow = () => {
|
|
72
99
|
state.expectedRestartInProgress = false;
|
|
@@ -337,6 +364,8 @@ const createWatchdog = ({
|
|
|
337
364
|
}
|
|
338
365
|
if (parsed.ok) {
|
|
339
366
|
const wasUnhealthy = state.health !== "healthy";
|
|
367
|
+
state.startupConsecutiveHealthFailures = 0;
|
|
368
|
+
clearDegradedHealthCheckTimer();
|
|
340
369
|
clearExpectedRestartWindow();
|
|
341
370
|
state.health = "healthy";
|
|
342
371
|
if (state.lifecycle !== "crash_loop") state.lifecycle = "running";
|
|
@@ -359,6 +388,8 @@ const createWatchdog = ({
|
|
|
359
388
|
return true;
|
|
360
389
|
}
|
|
361
390
|
if (restartWindowActive) {
|
|
391
|
+
state.startupConsecutiveHealthFailures = 0;
|
|
392
|
+
clearDegradedHealthCheckTimer();
|
|
362
393
|
logEvent(
|
|
363
394
|
"health_check",
|
|
364
395
|
source,
|
|
@@ -381,6 +412,8 @@ const createWatchdog = ({
|
|
|
381
412
|
state.lifecycle === "running" &&
|
|
382
413
|
!state.crashRecoveryActive;
|
|
383
414
|
if (withinStartupGrace) {
|
|
415
|
+
state.startupConsecutiveHealthFailures = 0;
|
|
416
|
+
clearDegradedHealthCheckTimer();
|
|
384
417
|
logEvent(
|
|
385
418
|
"health_check",
|
|
386
419
|
source,
|
|
@@ -397,7 +430,31 @@ const createWatchdog = ({
|
|
|
397
430
|
return false;
|
|
398
431
|
}
|
|
399
432
|
|
|
433
|
+
if (state.health === "unknown" && state.lifecycle === "running") {
|
|
434
|
+
state.startupConsecutiveHealthFailures += 1;
|
|
435
|
+
if (state.startupConsecutiveHealthFailures < kWatchdogStartupFailureThreshold) {
|
|
436
|
+
logEvent(
|
|
437
|
+
"health_check",
|
|
438
|
+
source,
|
|
439
|
+
"ok",
|
|
440
|
+
{
|
|
441
|
+
reason: parsed.reason,
|
|
442
|
+
result,
|
|
443
|
+
skipped: true,
|
|
444
|
+
startupFailureRetryActive: true,
|
|
445
|
+
startupConsecutiveFailures: state.startupConsecutiveHealthFailures,
|
|
446
|
+
startupFailureThreshold: kWatchdogStartupFailureThreshold,
|
|
447
|
+
},
|
|
448
|
+
correlationId,
|
|
449
|
+
);
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
} else {
|
|
453
|
+
state.startupConsecutiveHealthFailures = 0;
|
|
454
|
+
}
|
|
455
|
+
|
|
400
456
|
state.health = "degraded";
|
|
457
|
+
scheduleDegradedHealthCheck();
|
|
401
458
|
logEvent(
|
|
402
459
|
"health_check",
|
|
403
460
|
source,
|
|
@@ -435,6 +492,7 @@ const createWatchdog = ({
|
|
|
435
492
|
|
|
436
493
|
const onGatewayExit = ({ code, signal, expectedExit = false, stderrTail = [] } = {}) => {
|
|
437
494
|
const correlationId = createCorrelationId();
|
|
495
|
+
clearDegradedHealthCheckTimer();
|
|
438
496
|
if (expectedExit) {
|
|
439
497
|
state.lifecycle = "restarting";
|
|
440
498
|
state.health = "unknown";
|
|
@@ -504,8 +562,10 @@ const createWatchdog = ({
|
|
|
504
562
|
};
|
|
505
563
|
|
|
506
564
|
const onGatewayLaunch = ({ startedAt = Date.now(), pid = null } = {}) => {
|
|
565
|
+
clearDegradedHealthCheckTimer();
|
|
507
566
|
state.lifecycle = "running";
|
|
508
567
|
state.health = "unknown";
|
|
568
|
+
state.startupConsecutiveHealthFailures = 0;
|
|
509
569
|
state.crashRecoveryActive = false;
|
|
510
570
|
clearExpectedRestartWindow();
|
|
511
571
|
state.uptimeStartedAt = startedAt;
|
|
@@ -515,8 +575,10 @@ const createWatchdog = ({
|
|
|
515
575
|
};
|
|
516
576
|
|
|
517
577
|
const onExpectedRestart = () => {
|
|
578
|
+
clearDegradedHealthCheckTimer();
|
|
518
579
|
state.lifecycle = "restarting";
|
|
519
580
|
state.health = "unknown";
|
|
581
|
+
state.startupConsecutiveHealthFailures = 0;
|
|
520
582
|
state.crashRecoveryActive = false;
|
|
521
583
|
markExpectedRestartWindow();
|
|
522
584
|
startBootstrapHealthChecks();
|
|
@@ -533,14 +595,17 @@ const createWatchdog = ({
|
|
|
533
595
|
|
|
534
596
|
const start = () => {
|
|
535
597
|
if (healthTimer || bootstrapHealthTimer) return;
|
|
598
|
+
clearDegradedHealthCheckTimer();
|
|
536
599
|
state.lifecycle = "running";
|
|
537
600
|
state.health = "unknown";
|
|
601
|
+
state.startupConsecutiveHealthFailures = 0;
|
|
538
602
|
state.uptimeStartedAt = Date.now();
|
|
539
603
|
state.gatewayStartedAt = Date.now();
|
|
540
604
|
startBootstrapHealthChecks();
|
|
541
605
|
};
|
|
542
606
|
|
|
543
607
|
const stop = () => {
|
|
608
|
+
clearDegradedHealthCheckTimer();
|
|
544
609
|
if (bootstrapHealthTimer) {
|
|
545
610
|
clearTimeout(bootstrapHealthTimer);
|
|
546
611
|
bootstrapHealthTimer = null;
|
|
@@ -550,6 +615,7 @@ const createWatchdog = ({
|
|
|
550
615
|
healthTimer = null;
|
|
551
616
|
}
|
|
552
617
|
state.lifecycle = "stopped";
|
|
618
|
+
state.startupConsecutiveHealthFailures = 0;
|
|
553
619
|
};
|
|
554
620
|
|
|
555
621
|
const getStatus = () => {
|
|
@@ -3,6 +3,8 @@ const https = require("https");
|
|
|
3
3
|
const { URL } = require("url");
|
|
4
4
|
|
|
5
5
|
const kRedactedHeaderKeys = new Set(["authorization", "cookie", "x-webhook-token"]);
|
|
6
|
+
const kGmailDedupeTtlMs = 24 * 60 * 60 * 1000;
|
|
7
|
+
const kGmailDedupeCleanupIntervalMs = 60 * 1000;
|
|
6
8
|
|
|
7
9
|
const normalizeIp = (ip) => String(ip || "").replace(/^::ffff:/, "");
|
|
8
10
|
|
|
@@ -74,6 +76,51 @@ const resolveGatewayPath = ({ pathname, search }) => {
|
|
|
74
76
|
return `${pathname}${search || ""}`;
|
|
75
77
|
};
|
|
76
78
|
|
|
79
|
+
const parseJsonSafe = (rawValue) => {
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(String(rawValue || "").trim() || "{}");
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const getGmailPayloadData = (parsedBody) => {
|
|
88
|
+
if (!parsedBody || typeof parsedBody !== "object") return null;
|
|
89
|
+
if (parsedBody.payload && typeof parsedBody.payload === "object") {
|
|
90
|
+
return parsedBody.payload;
|
|
91
|
+
}
|
|
92
|
+
return parsedBody;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const getGmailMessageId = (message = {}) => {
|
|
96
|
+
const preferredId = String(message?.id || "").trim();
|
|
97
|
+
if (preferredId) return preferredId;
|
|
98
|
+
const fallbackId = String(message?.messageId || "").trim();
|
|
99
|
+
return fallbackId;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const buildGmailDedupedBodyBuffer = ({ parsedBody, filteredMessages }) => {
|
|
103
|
+
if (parsedBody?.payload && typeof parsedBody.payload === "object") {
|
|
104
|
+
return Buffer.from(
|
|
105
|
+
JSON.stringify({
|
|
106
|
+
...parsedBody,
|
|
107
|
+
payload: {
|
|
108
|
+
...parsedBody.payload,
|
|
109
|
+
messages: filteredMessages,
|
|
110
|
+
},
|
|
111
|
+
}),
|
|
112
|
+
"utf8",
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return Buffer.from(
|
|
116
|
+
JSON.stringify({
|
|
117
|
+
...(parsedBody || {}),
|
|
118
|
+
messages: filteredMessages,
|
|
119
|
+
}),
|
|
120
|
+
"utf8",
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
|
|
77
124
|
const createWebhookMiddleware = ({
|
|
78
125
|
gatewayUrl,
|
|
79
126
|
insertRequest,
|
|
@@ -81,6 +128,18 @@ const createWebhookMiddleware = ({
|
|
|
81
128
|
}) => {
|
|
82
129
|
const gateway = new URL(gatewayUrl);
|
|
83
130
|
const protocolClient = gateway.protocol === "https:" ? https : http;
|
|
131
|
+
const gmailSeenMessageIds = new Map();
|
|
132
|
+
let lastGmailDedupeCleanupAt = 0;
|
|
133
|
+
|
|
134
|
+
const pruneGmailSeenMessageIds = (nowMs) => {
|
|
135
|
+
if (nowMs - lastGmailDedupeCleanupAt < kGmailDedupeCleanupIntervalMs) return;
|
|
136
|
+
for (const [messageKey, seenAt] of gmailSeenMessageIds.entries()) {
|
|
137
|
+
if (nowMs - seenAt > kGmailDedupeTtlMs) {
|
|
138
|
+
gmailSeenMessageIds.delete(messageKey);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
lastGmailDedupeCleanupAt = nowMs;
|
|
142
|
+
};
|
|
84
143
|
|
|
85
144
|
return (req, res) => {
|
|
86
145
|
const inboundUrl = new URL(req.url, `http://${req.headers.host || "localhost"}`);
|
|
@@ -90,8 +149,47 @@ const createWebhookMiddleware = ({
|
|
|
90
149
|
inboundUrl.searchParams.delete("token");
|
|
91
150
|
}
|
|
92
151
|
|
|
93
|
-
|
|
152
|
+
let bodyBuffer = extractBodyBuffer(req);
|
|
94
153
|
const hookName = resolveHookName(req);
|
|
154
|
+
|
|
155
|
+
if (hookName === "gmail" && bodyBuffer.length > 0) {
|
|
156
|
+
const parsedBody = parseJsonSafe(bodyBuffer.toString("utf8"));
|
|
157
|
+
const payloadData = getGmailPayloadData(parsedBody);
|
|
158
|
+
const accountKey = String(
|
|
159
|
+
payloadData?.account || payloadData?.email || payloadData?.inbox || "unknown",
|
|
160
|
+
)
|
|
161
|
+
.trim()
|
|
162
|
+
.toLowerCase();
|
|
163
|
+
const messages = Array.isArray(payloadData?.messages) ? payloadData.messages : [];
|
|
164
|
+
if (messages.length > 0) {
|
|
165
|
+
const nowMs = Date.now();
|
|
166
|
+
pruneGmailSeenMessageIds(nowMs);
|
|
167
|
+
const unseenMessages = [];
|
|
168
|
+
for (const message of messages) {
|
|
169
|
+
const messageId = getGmailMessageId(message);
|
|
170
|
+
if (!messageId) {
|
|
171
|
+
unseenMessages.push(message);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const dedupeKey = `${accountKey}:${messageId}`;
|
|
175
|
+
if (gmailSeenMessageIds.has(dedupeKey)) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
gmailSeenMessageIds.set(dedupeKey, nowMs);
|
|
179
|
+
unseenMessages.push(message);
|
|
180
|
+
}
|
|
181
|
+
if (unseenMessages.length === 0) {
|
|
182
|
+
return res.status(200).json({ ok: true, deduped: true });
|
|
183
|
+
}
|
|
184
|
+
if (unseenMessages.length < messages.length && parsedBody) {
|
|
185
|
+
bodyBuffer = buildGmailDedupedBodyBuffer({
|
|
186
|
+
parsedBody,
|
|
187
|
+
filteredMessages: unseenMessages,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
95
193
|
const sourceIp = normalizeIp(
|
|
96
194
|
req.ip || req.headers["x-forwarded-for"] || req.socket?.remoteAddress || "",
|
|
97
195
|
);
|
package/lib/server/webhooks.js
CHANGED
|
@@ -2,6 +2,14 @@ const path = require("path");
|
|
|
2
2
|
|
|
3
3
|
const kNamePattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
4
4
|
const kTransformsDir = "hooks/transforms";
|
|
5
|
+
const kManagedWebhookConfigs = [
|
|
6
|
+
{
|
|
7
|
+
name: "gmail",
|
|
8
|
+
preset: "gmail",
|
|
9
|
+
description:
|
|
10
|
+
"Managed by AlphaClaw Gmail Watch setup. Required for internal Gmail watch delivery.",
|
|
11
|
+
},
|
|
12
|
+
];
|
|
5
13
|
|
|
6
14
|
const getConfigPath = ({ OPENCLAW_DIR }) =>
|
|
7
15
|
path.join(OPENCLAW_DIR, "openclaw.json");
|
|
@@ -45,7 +53,25 @@ const ensureHooksRoot = (cfg) => {
|
|
|
45
53
|
if (typeof cfg.hooks.path !== "string" || !cfg.hooks.path.trim())
|
|
46
54
|
cfg.hooks.path = "/hooks";
|
|
47
55
|
if (typeof cfg.hooks.token !== "string" || !cfg.hooks.token.trim()) {
|
|
48
|
-
cfg.hooks.token = "${
|
|
56
|
+
cfg.hooks.token = "${OPENCLAW_HOOKS_TOKEN}";
|
|
57
|
+
}
|
|
58
|
+
if (
|
|
59
|
+
typeof cfg.hooks.defaultSessionKey !== "string" ||
|
|
60
|
+
!cfg.hooks.defaultSessionKey.trim()
|
|
61
|
+
) {
|
|
62
|
+
cfg.hooks.defaultSessionKey = "hook:ingress";
|
|
63
|
+
}
|
|
64
|
+
if (typeof cfg.hooks.allowRequestSessionKey !== "boolean") {
|
|
65
|
+
cfg.hooks.allowRequestSessionKey = false;
|
|
66
|
+
}
|
|
67
|
+
if (!Array.isArray(cfg.hooks.allowedSessionKeyPrefixes)) {
|
|
68
|
+
cfg.hooks.allowedSessionKeyPrefixes = ["hook:"];
|
|
69
|
+
}
|
|
70
|
+
if (!cfg.hooks.allowedSessionKeyPrefixes.includes("hook:")) {
|
|
71
|
+
cfg.hooks.allowedSessionKeyPrefixes = [
|
|
72
|
+
...cfg.hooks.allowedSessionKeyPrefixes,
|
|
73
|
+
"hook:",
|
|
74
|
+
];
|
|
49
75
|
}
|
|
50
76
|
return cfg.hooks.mappings;
|
|
51
77
|
};
|
|
@@ -100,36 +126,155 @@ const normalizeMappingTransformModules = (mappings) => {
|
|
|
100
126
|
return changed;
|
|
101
127
|
};
|
|
102
128
|
|
|
129
|
+
const buildDefaultTransformSource = (name) =>
|
|
130
|
+
[
|
|
131
|
+
"export default async function transform(payload, context) {",
|
|
132
|
+
" const data = payload.payload || payload;",
|
|
133
|
+
" return {",
|
|
134
|
+
" message: data.message,",
|
|
135
|
+
` name: data.name || "${name}",`,
|
|
136
|
+
' wakeMode: data.wakeMode || "now",',
|
|
137
|
+
" };",
|
|
138
|
+
"}",
|
|
139
|
+
"",
|
|
140
|
+
].join("\n");
|
|
141
|
+
|
|
142
|
+
const ensureWebhookTransform = ({ fs, constants, name, source = "" }) => {
|
|
143
|
+
const webhookName = validateWebhookName(name);
|
|
144
|
+
const transformAbsolutePath = getTransformAbsolutePath(
|
|
145
|
+
constants,
|
|
146
|
+
webhookName,
|
|
147
|
+
);
|
|
148
|
+
fs.mkdirSync(path.dirname(transformAbsolutePath), { recursive: true });
|
|
149
|
+
if (fs.existsSync(transformAbsolutePath)) {
|
|
150
|
+
return { changed: false, path: transformAbsolutePath };
|
|
151
|
+
}
|
|
152
|
+
fs.writeFileSync(
|
|
153
|
+
transformAbsolutePath,
|
|
154
|
+
String(source || "").trim()
|
|
155
|
+
? `${String(source).replace(/\s+$/, "")}\n`
|
|
156
|
+
: buildDefaultTransformSource(webhookName),
|
|
157
|
+
);
|
|
158
|
+
return { changed: true, path: transformAbsolutePath };
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const ensureWebhookMapping = ({ cfg, name, mapping = {} }) => {
|
|
162
|
+
const webhookName = validateWebhookName(name);
|
|
163
|
+
const mappings = ensureHooksRoot(cfg);
|
|
164
|
+
const normalizedModulesChanged = normalizeMappingTransformModules(mappings);
|
|
165
|
+
const index = findMappingIndexByName(mappings, webhookName);
|
|
166
|
+
const defaults = {
|
|
167
|
+
match: { path: webhookName },
|
|
168
|
+
action: "agent",
|
|
169
|
+
name: webhookName,
|
|
170
|
+
wakeMode: "now",
|
|
171
|
+
transform: { module: getTransformModulePath(webhookName) },
|
|
172
|
+
};
|
|
173
|
+
if (index === -1) {
|
|
174
|
+
mappings.push({
|
|
175
|
+
...defaults,
|
|
176
|
+
...mapping,
|
|
177
|
+
match: { ...defaults.match, ...(mapping.match || {}) },
|
|
178
|
+
transform: { ...defaults.transform, ...(mapping.transform || {}) },
|
|
179
|
+
});
|
|
180
|
+
return { changed: true, created: true, normalizedModulesChanged };
|
|
181
|
+
}
|
|
182
|
+
const current = mappings[index] || {};
|
|
183
|
+
const next = {
|
|
184
|
+
...current,
|
|
185
|
+
...mapping,
|
|
186
|
+
match: {
|
|
187
|
+
...(current.match || {}),
|
|
188
|
+
...(mapping.match || {}),
|
|
189
|
+
path: webhookName,
|
|
190
|
+
},
|
|
191
|
+
action: mapping.action || current.action || defaults.action,
|
|
192
|
+
wakeMode: mapping.wakeMode || current.wakeMode || defaults.wakeMode,
|
|
193
|
+
transform: {
|
|
194
|
+
...(current.transform || {}),
|
|
195
|
+
...(mapping.transform || {}),
|
|
196
|
+
module:
|
|
197
|
+
String(mapping?.transform?.module || "").trim() ||
|
|
198
|
+
String(current?.transform?.module || "").trim() ||
|
|
199
|
+
defaults.transform.module,
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
if (JSON.stringify(current) !== JSON.stringify(next)) {
|
|
203
|
+
mappings[index] = next;
|
|
204
|
+
return { changed: true, created: false, normalizedModulesChanged };
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
changed: normalizedModulesChanged,
|
|
208
|
+
created: false,
|
|
209
|
+
normalizedModulesChanged,
|
|
210
|
+
};
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const listManagedWebhooksFromConfig = ({ cfg }) => {
|
|
214
|
+
const presets = Array.isArray(cfg?.hooks?.presets) ? cfg.hooks.presets : [];
|
|
215
|
+
return kManagedWebhookConfigs
|
|
216
|
+
.filter((managed) => presets.includes(managed.preset))
|
|
217
|
+
.map((managed) => ({
|
|
218
|
+
name: managed.name,
|
|
219
|
+
enabled: true,
|
|
220
|
+
createdAt: null,
|
|
221
|
+
path: `/hooks/${managed.name}`,
|
|
222
|
+
transformPath: null,
|
|
223
|
+
transformExists: true,
|
|
224
|
+
managed: true,
|
|
225
|
+
managedReason: managed.description,
|
|
226
|
+
}));
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const isManagedWebhook = ({ cfg, name }) => {
|
|
230
|
+
const normalized = String(name || "")
|
|
231
|
+
.trim()
|
|
232
|
+
.toLowerCase();
|
|
233
|
+
if (!normalized) return false;
|
|
234
|
+
return listManagedWebhooksFromConfig({ cfg }).some(
|
|
235
|
+
(webhook) => webhook.name === normalized,
|
|
236
|
+
);
|
|
237
|
+
};
|
|
238
|
+
|
|
103
239
|
const listWebhooks = ({ fs, constants }) => {
|
|
104
240
|
const { cfg } = readConfig({ fs, constants });
|
|
105
241
|
const mappings = ensureHooksRoot(cfg);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
.map((
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
242
|
+
const managedWebhooks = listManagedWebhooksFromConfig({ cfg });
|
|
243
|
+
const managedByName = new Map(
|
|
244
|
+
managedWebhooks.map((item) => [item.name, item]),
|
|
245
|
+
);
|
|
246
|
+
const mappingWebhooks = mappings.filter(isWebhookMapping).map((mapping) => {
|
|
247
|
+
const name = getMappingHookName(mapping);
|
|
248
|
+
const managed = managedByName.get(name);
|
|
249
|
+
const transformPath = resolveTransformPathFromMapping(name, mapping);
|
|
250
|
+
const transformAbsolutePath = path.join(
|
|
251
|
+
constants.OPENCLAW_DIR,
|
|
252
|
+
transformPath,
|
|
253
|
+
);
|
|
254
|
+
let createdAt = null;
|
|
255
|
+
try {
|
|
256
|
+
const stat = fs.statSync(transformAbsolutePath);
|
|
257
|
+
createdAt =
|
|
258
|
+
stat.birthtime?.toISOString?.() || stat.ctime?.toISOString?.() || null;
|
|
259
|
+
} catch {}
|
|
260
|
+
return {
|
|
261
|
+
name,
|
|
262
|
+
enabled: true,
|
|
263
|
+
createdAt,
|
|
264
|
+
path: `/hooks/${name}`,
|
|
265
|
+
transformPath,
|
|
266
|
+
transformExists: fs.existsSync(transformAbsolutePath),
|
|
267
|
+
managed: Boolean(managed),
|
|
268
|
+
managedReason: managed?.managedReason || "",
|
|
269
|
+
};
|
|
270
|
+
});
|
|
271
|
+
const mappingNames = new Set(mappingWebhooks.map((item) => item.name));
|
|
272
|
+
const syntheticManagedWebhooks = managedWebhooks.filter(
|
|
273
|
+
(item) => !mappingNames.has(item.name),
|
|
274
|
+
);
|
|
275
|
+
return [...mappingWebhooks, ...syntheticManagedWebhooks].sort((a, b) =>
|
|
276
|
+
a.name.localeCompare(b.name),
|
|
277
|
+
);
|
|
133
278
|
};
|
|
134
279
|
|
|
135
280
|
const getWebhookDetail = ({ fs, constants, name }) => {
|
|
@@ -137,6 +282,12 @@ const getWebhookDetail = ({ fs, constants, name }) => {
|
|
|
137
282
|
const hooks = listWebhooks({ fs, constants });
|
|
138
283
|
const detail = hooks.find((item) => item.name === webhookName);
|
|
139
284
|
if (!detail) return null;
|
|
285
|
+
if (detail.managed || !detail.transformPath) {
|
|
286
|
+
return {
|
|
287
|
+
...detail,
|
|
288
|
+
transformExists: true,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
140
291
|
const transformAbsolutePath = path.join(
|
|
141
292
|
constants.OPENCLAW_DIR,
|
|
142
293
|
detail.transformPath,
|
|
@@ -147,56 +298,54 @@ const getWebhookDetail = ({ fs, constants, name }) => {
|
|
|
147
298
|
};
|
|
148
299
|
};
|
|
149
300
|
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
" return {",
|
|
160
|
-
" message: data.message,",
|
|
161
|
-
` name: data.name || "${name}",`,
|
|
162
|
-
' wakeMode: data.wakeMode || "now",',
|
|
163
|
-
" };",
|
|
164
|
-
"}",
|
|
165
|
-
"",
|
|
166
|
-
].join("\n"),
|
|
167
|
-
);
|
|
168
|
-
return transformAbsolutePath;
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
const createWebhook = ({ fs, constants, name }) => {
|
|
301
|
+
const createWebhook = ({
|
|
302
|
+
fs,
|
|
303
|
+
constants,
|
|
304
|
+
name,
|
|
305
|
+
upsert = false,
|
|
306
|
+
allowManagedName = false,
|
|
307
|
+
mapping = {},
|
|
308
|
+
transformSource = "",
|
|
309
|
+
}) => {
|
|
172
310
|
const webhookName = validateWebhookName(name);
|
|
173
311
|
const { cfg, configPath } = readConfig({ fs, constants });
|
|
174
|
-
if (!
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
312
|
+
if (!allowManagedName && isManagedWebhook({ cfg, name: webhookName })) {
|
|
313
|
+
throw new Error(
|
|
314
|
+
`Webhook "${webhookName}" is managed and cannot be created manually`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
const existingMappings = ensureHooksRoot(cfg);
|
|
318
|
+
const exists = findMappingIndexByName(existingMappings, webhookName) !== -1;
|
|
319
|
+
if (exists && !upsert) {
|
|
178
320
|
throw new Error(`Webhook "${webhookName}" already exists`);
|
|
179
321
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
action: "agent",
|
|
322
|
+
const ensuredMapping = ensureWebhookMapping({
|
|
323
|
+
cfg,
|
|
183
324
|
name: webhookName,
|
|
184
|
-
|
|
185
|
-
transform: { module: getTransformModulePath(webhookName) },
|
|
325
|
+
mapping,
|
|
186
326
|
});
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
327
|
+
const ensuredTransform = ensureWebhookTransform({
|
|
328
|
+
fs,
|
|
329
|
+
constants,
|
|
330
|
+
name: webhookName,
|
|
331
|
+
source: transformSource,
|
|
332
|
+
});
|
|
333
|
+
if (ensuredMapping.changed || ensuredTransform.changed || !exists) {
|
|
334
|
+
writeConfig({ fs, configPath, cfg });
|
|
191
335
|
}
|
|
192
|
-
writeConfig({ fs, configPath, cfg });
|
|
193
|
-
ensureStarterTransform({ fs, constants, name: webhookName });
|
|
194
336
|
return getWebhookDetail({ fs, constants, name: webhookName });
|
|
195
337
|
};
|
|
196
338
|
|
|
197
339
|
const deleteWebhook = ({ fs, constants, name, deleteTransformDir = false }) => {
|
|
198
340
|
const webhookName = validateWebhookName(name);
|
|
199
341
|
const { cfg, configPath } = readConfig({ fs, constants });
|
|
342
|
+
if (isManagedWebhook({ cfg, name: webhookName })) {
|
|
343
|
+
return {
|
|
344
|
+
removed: false,
|
|
345
|
+
managed: true,
|
|
346
|
+
deletedTransformDir: false,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
200
349
|
const mappings = ensureHooksRoot(cfg);
|
|
201
350
|
const normalizedModules = normalizeMappingTransformModules(mappings);
|
|
202
351
|
const index = findMappingIndexByName(mappings, webhookName);
|
package/lib/server.js
CHANGED
|
@@ -98,6 +98,7 @@ const { registerTelegramRoutes } = require("./server/routes/telegram");
|
|
|
98
98
|
const { registerWebhookRoutes } = require("./server/routes/webhooks");
|
|
99
99
|
const { registerWatchdogRoutes } = require("./server/routes/watchdog");
|
|
100
100
|
const { registerUsageRoutes } = require("./server/routes/usage");
|
|
101
|
+
const { registerGmailRoutes } = require("./server/routes/gmail");
|
|
101
102
|
|
|
102
103
|
const { PORT, GATEWAY_URL, kTrustProxyHops, SETUP_API_PREFIXES } = constants;
|
|
103
104
|
|
|
@@ -111,6 +112,7 @@ migrateManagedInternalFiles({
|
|
|
111
112
|
const app = express();
|
|
112
113
|
app.set("trust proxy", kTrustProxyHops);
|
|
113
114
|
app.use(["/webhook", "/hooks"], express.raw({ type: "*/*", limit: "5mb" }));
|
|
115
|
+
app.use("/gmail-pubsub", express.raw({ type: "*/*", limit: "5mb" }));
|
|
114
116
|
app.use(express.json());
|
|
115
117
|
|
|
116
118
|
const proxy = httpProxy.createProxyServer({
|
|
@@ -241,6 +243,18 @@ registerGoogleRoutes({
|
|
|
241
243
|
getApiEnableUrl,
|
|
242
244
|
constants,
|
|
243
245
|
});
|
|
246
|
+
const gmailWatchService = registerGmailRoutes({
|
|
247
|
+
app,
|
|
248
|
+
fs,
|
|
249
|
+
constants,
|
|
250
|
+
gogCmd,
|
|
251
|
+
getBaseUrl,
|
|
252
|
+
readGoogleCredentials,
|
|
253
|
+
readEnvFile,
|
|
254
|
+
writeEnvFile,
|
|
255
|
+
reloadEnv,
|
|
256
|
+
restartRequiredState,
|
|
257
|
+
});
|
|
244
258
|
const telegramApi = createTelegramApi(() => process.env.TELEGRAM_BOT_TOKEN);
|
|
245
259
|
const discordApi = createDiscordApi(() => process.env.DISCORD_BOT_TOKEN);
|
|
246
260
|
const watchdogNotifier = createWatchdogNotifier({ telegramApi, discordApi });
|
|
@@ -345,7 +359,20 @@ server.listen(PORT, "0.0.0.0", () => {
|
|
|
345
359
|
ensureGatewayProxyConfig(null);
|
|
346
360
|
startGateway();
|
|
347
361
|
watchdog.start();
|
|
362
|
+
gmailWatchService.start();
|
|
348
363
|
} else {
|
|
349
364
|
console.log("[alphaclaw] Awaiting onboarding via Setup UI");
|
|
350
365
|
}
|
|
351
366
|
});
|
|
367
|
+
|
|
368
|
+
const shutdownGmailWatchService = async () => {
|
|
369
|
+
try {
|
|
370
|
+
await gmailWatchService.stop();
|
|
371
|
+
} catch {}
|
|
372
|
+
};
|
|
373
|
+
process.on("SIGTERM", () => {
|
|
374
|
+
shutdownGmailWatchService();
|
|
375
|
+
});
|
|
376
|
+
process.on("SIGINT", () => {
|
|
377
|
+
shutdownGmailWatchService();
|
|
378
|
+
});
|
package/lib/setup/gitignore
CHANGED
|
@@ -6,10 +6,16 @@
|
|
|
6
6
|
!workspace/**
|
|
7
7
|
workspace/.openclaw/
|
|
8
8
|
workspace/.openclaw/**
|
|
9
|
+
!gogcli/
|
|
10
|
+
gogcli/*
|
|
11
|
+
!gogcli/state.json
|
|
9
12
|
db/
|
|
10
13
|
db/**
|
|
11
14
|
!skills/
|
|
12
15
|
!skills/**
|
|
16
|
+
!hooks/
|
|
17
|
+
!hooks/transforms/
|
|
18
|
+
!hooks/transforms/**
|
|
13
19
|
!cron/
|
|
14
20
|
!cron/jobs.json
|
|
15
21
|
!openclaw.json
|