@chrysb/alphaclaw 0.4.0 → 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/lib/public/css/shell.css +21 -19
- package/lib/public/css/theme.css +17 -0
- package/lib/public/js/app.js +80 -5
- package/lib/public/js/components/file-viewer/editor-surface.js +5 -0
- package/lib/public/js/components/file-viewer/index.js +3 -0
- package/lib/public/js/components/file-viewer/markdown-split-view.js +2 -0
- package/lib/public/js/components/file-viewer/toolbar.js +13 -0
- package/lib/public/js/components/file-viewer/use-file-viewer.js +48 -13
- package/lib/public/js/components/google/account-row.js +34 -1
- 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 +118 -4
- package/lib/public/js/components/google/use-gmail-watch.js +140 -0
- package/lib/public/js/components/scope-picker.js +1 -1
- package/lib/public/js/components/sidebar-git-panel.js +5 -6
- package/lib/public/js/components/sidebar.js +2 -0
- 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 +106 -1
- package/lib/public/js/lib/format.js +71 -0
- package/lib/server/constants.js +28 -0
- 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 +130 -0
- package/lib/server/helpers.js +5 -7
- package/lib/server/internal-files-migration.js +31 -3
- package/lib/server/routes/gmail.js +128 -0
- package/lib/server/routes/google.js +19 -0
- 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 +3 -0
- package/lib/setup/hourly-git-sync.sh +1 -1
- package/package.json +1 -1
- package/lib/public/js/components/usage-tab.js +0 -531
|
@@ -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