@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/public/js/lib/api.js
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
|
+
const kClientTimeZoneHeader = "x-client-timezone";
|
|
2
|
+
|
|
3
|
+
const getBrowserTimeZone = () => {
|
|
4
|
+
try {
|
|
5
|
+
return Intl?.DateTimeFormat?.().resolvedOptions?.().timeZone || "";
|
|
6
|
+
} catch {
|
|
7
|
+
return "";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
|
|
1
11
|
export const authFetch = async (url, opts = {}) => {
|
|
2
|
-
const
|
|
12
|
+
const nextOptions = { ...opts };
|
|
13
|
+
const headers = new Headers(opts?.headers || {});
|
|
14
|
+
if (!headers.has(kClientTimeZoneHeader)) {
|
|
15
|
+
const browserTimeZone = getBrowserTimeZone();
|
|
16
|
+
if (browserTimeZone) {
|
|
17
|
+
headers.set(kClientTimeZoneHeader, browserTimeZone);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
nextOptions.headers = headers;
|
|
21
|
+
const res = await fetch(url, nextOptions);
|
|
3
22
|
if (res.status === 401) {
|
|
4
23
|
try {
|
|
5
24
|
window.localStorage?.clear?.();
|
|
@@ -38,30 +57,162 @@ export async function rejectPairing(id, channel) {
|
|
|
38
57
|
return res.json();
|
|
39
58
|
}
|
|
40
59
|
|
|
41
|
-
export async function
|
|
42
|
-
const res = await authFetch('/api/google/
|
|
60
|
+
export async function fetchGoogleAccounts() {
|
|
61
|
+
const res = await authFetch('/api/google/accounts');
|
|
62
|
+
return res.json();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function fetchGoogleStatus(accountId = "") {
|
|
66
|
+
const params = new URLSearchParams();
|
|
67
|
+
if (accountId) params.set("accountId", String(accountId));
|
|
68
|
+
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
69
|
+
const res = await authFetch(`/api/google/status${suffix}`);
|
|
70
|
+
return res.json();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function fetchGoogleCredentials({
|
|
74
|
+
accountId = "",
|
|
75
|
+
client = "",
|
|
76
|
+
} = {}) {
|
|
77
|
+
const params = new URLSearchParams();
|
|
78
|
+
if (accountId) params.set("accountId", String(accountId));
|
|
79
|
+
if (client) params.set("client", String(client));
|
|
80
|
+
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
81
|
+
const res = await authFetch(`/api/google/credentials${suffix}`);
|
|
43
82
|
return res.json();
|
|
44
83
|
}
|
|
45
84
|
|
|
46
|
-
export async function checkGoogleApis() {
|
|
47
|
-
const
|
|
85
|
+
export async function checkGoogleApis(accountId = "") {
|
|
86
|
+
const params = new URLSearchParams();
|
|
87
|
+
if (accountId) params.set("accountId", String(accountId));
|
|
88
|
+
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
89
|
+
const res = await authFetch(`/api/google/check${suffix}`);
|
|
48
90
|
return res.json();
|
|
49
91
|
}
|
|
50
92
|
|
|
51
|
-
export async function saveGoogleCredentials(
|
|
93
|
+
export async function saveGoogleCredentials({
|
|
94
|
+
clientId,
|
|
95
|
+
clientSecret,
|
|
96
|
+
email,
|
|
97
|
+
services = [],
|
|
98
|
+
client = "default",
|
|
99
|
+
personal = false,
|
|
100
|
+
accountId = "",
|
|
101
|
+
}) {
|
|
52
102
|
const res = await authFetch('/api/google/credentials', {
|
|
53
103
|
method: 'POST',
|
|
54
104
|
headers: { 'Content-Type': 'application/json' },
|
|
55
|
-
body: JSON.stringify({
|
|
105
|
+
body: JSON.stringify({
|
|
106
|
+
clientId,
|
|
107
|
+
clientSecret,
|
|
108
|
+
email,
|
|
109
|
+
services,
|
|
110
|
+
client,
|
|
111
|
+
personal,
|
|
112
|
+
accountId,
|
|
113
|
+
}),
|
|
56
114
|
});
|
|
57
115
|
return res.json();
|
|
58
116
|
}
|
|
59
117
|
|
|
60
|
-
export async function
|
|
61
|
-
|
|
118
|
+
export async function saveGoogleAccount({
|
|
119
|
+
email,
|
|
120
|
+
services = [],
|
|
121
|
+
client = "default",
|
|
122
|
+
personal = false,
|
|
123
|
+
accountId = "",
|
|
124
|
+
}) {
|
|
125
|
+
const res = await authFetch('/api/google/accounts', {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
headers: { 'Content-Type': 'application/json' },
|
|
128
|
+
body: JSON.stringify({ email, services, client, personal, accountId }),
|
|
129
|
+
});
|
|
62
130
|
return res.json();
|
|
63
131
|
}
|
|
64
132
|
|
|
133
|
+
export async function disconnectGoogle(accountId = "") {
|
|
134
|
+
const res = await authFetch('/api/google/disconnect', {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: { 'Content-Type': 'application/json' },
|
|
137
|
+
body: JSON.stringify({ accountId }),
|
|
138
|
+
});
|
|
139
|
+
return res.json();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const fetchGmailConfig = async () => {
|
|
143
|
+
const res = await authFetch("/api/gmail/config");
|
|
144
|
+
return parseJsonOrThrow(res, "Could not load Gmail watch config");
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export const saveGmailConfig = async ({
|
|
148
|
+
client = "default",
|
|
149
|
+
topicPath = "",
|
|
150
|
+
projectId = "",
|
|
151
|
+
regeneratePushToken = false,
|
|
152
|
+
} = {}) => {
|
|
153
|
+
const res = await authFetch("/api/gmail/config", {
|
|
154
|
+
method: "POST",
|
|
155
|
+
headers: { "Content-Type": "application/json" },
|
|
156
|
+
body: JSON.stringify({
|
|
157
|
+
client,
|
|
158
|
+
topicPath,
|
|
159
|
+
projectId,
|
|
160
|
+
regeneratePushToken,
|
|
161
|
+
}),
|
|
162
|
+
});
|
|
163
|
+
return parseJsonOrThrow(res, "Could not save Gmail watch config");
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export const startGmailWatch = async (accountId) => {
|
|
167
|
+
const res = await authFetch("/api/gmail/watch/start", {
|
|
168
|
+
method: "POST",
|
|
169
|
+
headers: { "Content-Type": "application/json" },
|
|
170
|
+
body: JSON.stringify({ accountId: String(accountId || "") }),
|
|
171
|
+
});
|
|
172
|
+
return parseJsonOrThrow(res, "Could not start Gmail watch");
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export const stopGmailWatch = async (accountId) => {
|
|
176
|
+
const res = await authFetch("/api/gmail/watch/stop", {
|
|
177
|
+
method: "POST",
|
|
178
|
+
headers: { "Content-Type": "application/json" },
|
|
179
|
+
body: JSON.stringify({ accountId: String(accountId || "") }),
|
|
180
|
+
});
|
|
181
|
+
return parseJsonOrThrow(res, "Could not stop Gmail watch");
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export const renewGmailWatch = async ({
|
|
185
|
+
accountId = "",
|
|
186
|
+
force = true,
|
|
187
|
+
} = {}) => {
|
|
188
|
+
const res = await authFetch("/api/gmail/watch/renew", {
|
|
189
|
+
method: "POST",
|
|
190
|
+
headers: { "Content-Type": "application/json" },
|
|
191
|
+
body: JSON.stringify({ accountId: String(accountId || ""), force: Boolean(force) }),
|
|
192
|
+
});
|
|
193
|
+
return parseJsonOrThrow(res, "Could not renew Gmail watch");
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export const fetchAgentSessions = async () => {
|
|
197
|
+
const res = await authFetch("/api/agent/sessions");
|
|
198
|
+
return parseJsonOrThrow(res, "Could not load agent sessions");
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export const sendAgentMessage = async ({
|
|
202
|
+
message = "",
|
|
203
|
+
sessionKey = "",
|
|
204
|
+
} = {}) => {
|
|
205
|
+
const res = await authFetch("/api/agent/message", {
|
|
206
|
+
method: "POST",
|
|
207
|
+
headers: { "Content-Type": "application/json" },
|
|
208
|
+
body: JSON.stringify({
|
|
209
|
+
message: String(message || ""),
|
|
210
|
+
sessionKey: String(sessionKey || ""),
|
|
211
|
+
}),
|
|
212
|
+
});
|
|
213
|
+
return parseJsonOrThrow(res, "Could not send message to agent");
|
|
214
|
+
};
|
|
215
|
+
|
|
65
216
|
export async function restartGateway() {
|
|
66
217
|
const res = await authFetch('/api/gateway/restart', { method: 'POST' });
|
|
67
218
|
return parseJsonOrThrow(res, 'Could not restart gateway');
|
|
@@ -389,6 +540,24 @@ export const saveFileContent = async (filePath, content) => {
|
|
|
389
540
|
return parseJsonOrThrow(res, 'Could not save file');
|
|
390
541
|
};
|
|
391
542
|
|
|
543
|
+
export const deleteBrowseFile = async (filePath) => {
|
|
544
|
+
const res = await authFetch('/api/browse/delete', {
|
|
545
|
+
method: 'DELETE',
|
|
546
|
+
headers: { 'Content-Type': 'application/json' },
|
|
547
|
+
body: JSON.stringify({ path: String(filePath || '') }),
|
|
548
|
+
});
|
|
549
|
+
return parseJsonOrThrow(res, 'Could not delete file');
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
export const restoreBrowseFile = async (filePath) => {
|
|
553
|
+
const res = await authFetch('/api/browse/restore', {
|
|
554
|
+
method: 'POST',
|
|
555
|
+
headers: { 'Content-Type': 'application/json' },
|
|
556
|
+
body: JSON.stringify({ path: String(filePath || '') }),
|
|
557
|
+
});
|
|
558
|
+
return parseJsonOrThrow(res, 'Could not restore file');
|
|
559
|
+
};
|
|
560
|
+
|
|
392
561
|
export const fetchBrowseGitSummary = async () => {
|
|
393
562
|
const res = await authFetch('/api/browse/git-summary');
|
|
394
563
|
return parseJsonOrThrow(res, 'Could not load git summary');
|
|
@@ -1,15 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
"
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
const kBrowseFilePoliciesUrl = new URL(
|
|
2
|
+
"../../shared/browse-file-policies.json",
|
|
3
|
+
import.meta.url,
|
|
4
|
+
);
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
let kBrowseFilePolicies = {
|
|
7
|
+
protectedPaths: [],
|
|
8
|
+
lockedPaths: [],
|
|
9
|
+
};
|
|
10
|
+
try {
|
|
11
|
+
const policyResponse = await fetch(kBrowseFilePoliciesUrl);
|
|
12
|
+
if (policyResponse.ok) {
|
|
13
|
+
const policyJson = await policyResponse.json();
|
|
14
|
+
if (policyJson && typeof policyJson === "object") {
|
|
15
|
+
kBrowseFilePolicies = policyJson;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
} catch {}
|
|
19
|
+
|
|
20
|
+
export const kProtectedBrowsePaths = new Set(
|
|
21
|
+
Array.isArray(kBrowseFilePolicies?.protectedPaths)
|
|
22
|
+
? kBrowseFilePolicies.protectedPaths
|
|
23
|
+
: [],
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export const kLockedBrowsePaths = new Set(
|
|
27
|
+
Array.isArray(kBrowseFilePolicies?.lockedPaths)
|
|
28
|
+
? kBrowseFilePolicies.lockedPaths
|
|
29
|
+
: [],
|
|
30
|
+
);
|
|
13
31
|
|
|
14
32
|
export const normalizeBrowsePolicyPath = (inputPath) =>
|
|
15
33
|
String(inputPath || "")
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const kIntegerFormatter = new Intl.NumberFormat("en-US");
|
|
2
|
+
const kUsdFormatter = new Intl.NumberFormat("en-US", {
|
|
3
|
+
style: "currency",
|
|
4
|
+
currency: "USD",
|
|
5
|
+
minimumFractionDigits: 2,
|
|
6
|
+
maximumFractionDigits: 3,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const toDateValue = (
|
|
10
|
+
value,
|
|
11
|
+
{ valueIsUnixSeconds = false, valueIsEpochMs = false } = {},
|
|
12
|
+
) => {
|
|
13
|
+
if (value == null || value === "") return null;
|
|
14
|
+
if (value instanceof Date) return value;
|
|
15
|
+
if (valueIsUnixSeconds) return new Date(Number(value) * 1000);
|
|
16
|
+
if (valueIsEpochMs) return new Date(Number(value));
|
|
17
|
+
return new Date(value);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const isSameDay = (left, right) =>
|
|
21
|
+
left.getFullYear() === right.getFullYear() &&
|
|
22
|
+
left.getMonth() === right.getMonth() &&
|
|
23
|
+
left.getDate() === right.getDate();
|
|
24
|
+
|
|
25
|
+
export const formatInteger = (value) =>
|
|
26
|
+
kIntegerFormatter.format(Number(value || 0));
|
|
27
|
+
|
|
28
|
+
export const formatUsd = (value) => kUsdFormatter.format(Number(value || 0));
|
|
29
|
+
|
|
30
|
+
export const formatLocaleDateTime = (
|
|
31
|
+
value,
|
|
32
|
+
{ fallback = "—", valueIsUnixSeconds = false, valueIsEpochMs = false } = {},
|
|
33
|
+
) => {
|
|
34
|
+
try {
|
|
35
|
+
const dateValue = toDateValue(value, { valueIsUnixSeconds, valueIsEpochMs });
|
|
36
|
+
if (!dateValue || Number.isNaN(dateValue.getTime())) return fallback;
|
|
37
|
+
return dateValue.toLocaleString();
|
|
38
|
+
} catch {
|
|
39
|
+
return fallback;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const formatLocaleDateTimeWithTodayTime = (
|
|
44
|
+
value,
|
|
45
|
+
{
|
|
46
|
+
fallback = "—",
|
|
47
|
+
valueIsUnixSeconds = false,
|
|
48
|
+
valueIsEpochMs = false,
|
|
49
|
+
} = {},
|
|
50
|
+
) => {
|
|
51
|
+
try {
|
|
52
|
+
const dateValue = toDateValue(value, { valueIsUnixSeconds, valueIsEpochMs });
|
|
53
|
+
if (!dateValue || Number.isNaN(dateValue.getTime())) return fallback;
|
|
54
|
+
return isSameDay(dateValue, new Date())
|
|
55
|
+
? dateValue.toLocaleTimeString()
|
|
56
|
+
: dateValue.toLocaleString();
|
|
57
|
+
} catch {
|
|
58
|
+
return fallback;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const formatDurationCompactMs = (value) => {
|
|
63
|
+
const ms = Number(value || 0);
|
|
64
|
+
if (!Number.isFinite(ms) || ms <= 0) return "0s";
|
|
65
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
66
|
+
const totalSeconds = Math.round(ms / 1000);
|
|
67
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
68
|
+
const seconds = totalSeconds % 60;
|
|
69
|
+
if (minutes <= 0) return `${seconds}s`;
|
|
70
|
+
return `${minutes}m ${seconds}s`;
|
|
71
|
+
};
|
|
@@ -8,11 +8,12 @@ import { escapeHtml, toLineObjects } from "./utils.js";
|
|
|
8
8
|
|
|
9
9
|
export const getFileSyntaxKind = (filePath) => {
|
|
10
10
|
const normalizedPath = String(filePath || "").toLowerCase();
|
|
11
|
-
|
|
12
|
-
if (/\.(
|
|
13
|
-
if (/\.(
|
|
14
|
-
if (/\.(
|
|
15
|
-
if (/\.(
|
|
11
|
+
const pathWithoutBakSuffix = normalizedPath.replace(/(\.bak)+$/i, "");
|
|
12
|
+
if (/\.(md|markdown|mdx)$/i.test(pathWithoutBakSuffix)) return "markdown";
|
|
13
|
+
if (/\.(json|jsonl)$/i.test(pathWithoutBakSuffix)) return "json";
|
|
14
|
+
if (/\.(html|htm)$/i.test(pathWithoutBakSuffix)) return "html";
|
|
15
|
+
if (/\.(js|mjs|cjs)$/i.test(pathWithoutBakSuffix)) return "javascript";
|
|
16
|
+
if (/\.(css|scss)$/i.test(pathWithoutBakSuffix)) return "css";
|
|
16
17
|
return "plain";
|
|
17
18
|
};
|
|
18
19
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"protectedPaths": [
|
|
3
|
+
"openclaw.json",
|
|
4
|
+
"devices/paired.json"
|
|
5
|
+
],
|
|
6
|
+
"lockedPaths": [
|
|
7
|
+
"hooks/bootstrap/agents.md",
|
|
8
|
+
"hooks/bootstrap/tools.md",
|
|
9
|
+
"skills/control-ui/skill.md",
|
|
10
|
+
".alphaclaw/hourly-git-sync.sh",
|
|
11
|
+
".alphaclaw/.cli-device-auto-approved"
|
|
12
|
+
]
|
|
13
|
+
}
|
package/lib/server/constants.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const os = require("os");
|
|
2
2
|
const path = require("path");
|
|
3
|
+
const kBrowseFilePolicies = require("../public/shared/browse-file-policies.json");
|
|
3
4
|
|
|
4
5
|
const parsePositiveIntEnv = (value, fallbackValue) => {
|
|
5
6
|
const parsed = Number.parseInt(String(value || ""), 10);
|
|
@@ -119,6 +120,12 @@ const kMaxPayloadBytes = parsePositiveIntEnv(process.env.WEBHOOK_LOG_MAX_BYTES,
|
|
|
119
120
|
const kWebhookPruneDays = parsePositiveIntEnv(process.env.WEBHOOK_LOG_RETENTION_DAYS, 30);
|
|
120
121
|
const kWatchdogCheckIntervalMs =
|
|
121
122
|
parsePositiveIntEnv(process.env.WATCHDOG_CHECK_INTERVAL, 120) * 1000;
|
|
123
|
+
const kWatchdogDegradedCheckIntervalMs =
|
|
124
|
+
parsePositiveIntEnv(process.env.WATCHDOG_DEGRADED_CHECK_INTERVAL, 5) * 1000;
|
|
125
|
+
const kWatchdogStartupFailureThreshold = parsePositiveIntEnv(
|
|
126
|
+
process.env.WATCHDOG_STARTUP_FAILURE_THRESHOLD,
|
|
127
|
+
3,
|
|
128
|
+
);
|
|
122
129
|
const kWatchdogMaxRepairAttempts = parsePositiveIntEnv(
|
|
123
130
|
process.env.WATCHDOG_MAX_REPAIR_ATTEMPTS,
|
|
124
131
|
2,
|
|
@@ -140,6 +147,7 @@ const kLogMaxBytes = parsePositiveIntEnv(
|
|
|
140
147
|
|
|
141
148
|
const kSystemVars = new Set([
|
|
142
149
|
"WEBHOOK_TOKEN",
|
|
150
|
+
"OPENCLAW_HOOKS_TOKEN",
|
|
143
151
|
"OPENCLAW_GATEWAY_TOKEN",
|
|
144
152
|
"SETUP_PASSWORD",
|
|
145
153
|
"PORT",
|
|
@@ -258,6 +266,25 @@ const GOG_CONFIG_DIR = path.join(OPENCLAW_DIR, "gogcli");
|
|
|
258
266
|
const GOG_CREDENTIALS_PATH = path.join(GOG_CONFIG_DIR, "credentials.json");
|
|
259
267
|
const GOG_STATE_PATH = path.join(GOG_CONFIG_DIR, "state.json");
|
|
260
268
|
const GOG_KEYRING_PASSWORD = process.env.GOG_KEYRING_PASSWORD || "alphaclaw";
|
|
269
|
+
const kMaxGoogleAccounts = 5;
|
|
270
|
+
const kGmailServeBasePort = parsePositiveIntEnv(
|
|
271
|
+
process.env.GMAIL_SERVE_BASE_PORT,
|
|
272
|
+
18801,
|
|
273
|
+
);
|
|
274
|
+
const kGmailWatchRenewalIntervalMs =
|
|
275
|
+
parsePositiveIntEnv(process.env.GMAIL_WATCH_RENEWAL_INTERVAL_SECONDS, 6 * 60 * 60) *
|
|
276
|
+
1000;
|
|
277
|
+
const kGmailWatchRenewalThresholdMs =
|
|
278
|
+
parsePositiveIntEnv(process.env.GMAIL_WATCH_RENEWAL_THRESHOLD_SECONDS, 24 * 60 * 60) *
|
|
279
|
+
1000;
|
|
280
|
+
const kGmailMaxBodyBytes = parsePositiveIntEnv(
|
|
281
|
+
process.env.GMAIL_WATCH_MAX_BODY_BYTES,
|
|
282
|
+
20000,
|
|
283
|
+
);
|
|
284
|
+
const gogClientCredentialsPath = (clientName = "default") =>
|
|
285
|
+
clientName === "default"
|
|
286
|
+
? GOG_CREDENTIALS_PATH
|
|
287
|
+
: path.join(GOG_CONFIG_DIR, `credentials-${clientName}.json`);
|
|
261
288
|
|
|
262
289
|
const API_TEST_COMMANDS = {
|
|
263
290
|
gmail: "gmail labels list",
|
|
@@ -274,13 +301,16 @@ const kChannelDefs = {
|
|
|
274
301
|
telegram: { envKey: "TELEGRAM_BOT_TOKEN" },
|
|
275
302
|
discord: { envKey: "DISCORD_BOT_TOKEN" },
|
|
276
303
|
};
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
304
|
+
const kProtectedBrowsePaths = new Set(
|
|
305
|
+
Array.isArray(kBrowseFilePolicies?.protectedPaths)
|
|
306
|
+
? kBrowseFilePolicies.protectedPaths
|
|
307
|
+
: [],
|
|
308
|
+
);
|
|
309
|
+
const kLockedBrowsePaths = new Set(
|
|
310
|
+
Array.isArray(kBrowseFilePolicies?.lockedPaths)
|
|
311
|
+
? kBrowseFilePolicies.lockedPaths
|
|
312
|
+
: [],
|
|
313
|
+
);
|
|
284
314
|
|
|
285
315
|
const SETUP_API_PREFIXES = [
|
|
286
316
|
"/api/status",
|
|
@@ -299,6 +329,7 @@ const SETUP_API_PREFIXES = [
|
|
|
299
329
|
"/api/sync-cron",
|
|
300
330
|
"/api/telegram",
|
|
301
331
|
"/api/webhooks",
|
|
332
|
+
"/api/gmail",
|
|
302
333
|
"/api/watchdog",
|
|
303
334
|
"/api/usage",
|
|
304
335
|
];
|
|
@@ -342,6 +373,8 @@ module.exports = {
|
|
|
342
373
|
kMaxPayloadBytes,
|
|
343
374
|
kWebhookPruneDays,
|
|
344
375
|
kWatchdogCheckIntervalMs,
|
|
376
|
+
kWatchdogDegradedCheckIntervalMs,
|
|
377
|
+
kWatchdogStartupFailureThreshold,
|
|
345
378
|
kWatchdogMaxRepairAttempts,
|
|
346
379
|
kWatchdogCrashLoopWindowMs,
|
|
347
380
|
kWatchdogCrashLoopThreshold,
|
|
@@ -350,6 +383,7 @@ module.exports = {
|
|
|
350
383
|
kSystemVars,
|
|
351
384
|
kKnownVars,
|
|
352
385
|
kKnownKeys,
|
|
386
|
+
kProtectedBrowsePaths,
|
|
353
387
|
kLockedBrowsePaths,
|
|
354
388
|
SCOPE_MAP,
|
|
355
389
|
REVERSE_SCOPE_MAP,
|
|
@@ -358,6 +392,12 @@ module.exports = {
|
|
|
358
392
|
GOG_CREDENTIALS_PATH,
|
|
359
393
|
GOG_STATE_PATH,
|
|
360
394
|
GOG_KEYRING_PASSWORD,
|
|
395
|
+
kMaxGoogleAccounts,
|
|
396
|
+
kGmailServeBasePort,
|
|
397
|
+
kGmailWatchRenewalIntervalMs,
|
|
398
|
+
kGmailWatchRenewalThresholdMs,
|
|
399
|
+
kGmailMaxBodyBytes,
|
|
400
|
+
gogClientCredentialsPath,
|
|
361
401
|
API_TEST_COMMANDS,
|
|
362
402
|
kChannelDefs,
|
|
363
403
|
SETUP_API_PREFIXES,
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const http = require("http");
|
|
2
|
+
|
|
3
|
+
const extractBodyBuffer = (body) => {
|
|
4
|
+
if (Buffer.isBuffer(body)) return body;
|
|
5
|
+
if (typeof body === "string") return Buffer.from(body, "utf8");
|
|
6
|
+
if (body && typeof body === "object") {
|
|
7
|
+
return Buffer.from(JSON.stringify(body), "utf8");
|
|
8
|
+
}
|
|
9
|
+
return Buffer.alloc(0);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const parsePushEnvelope = (bodyBuffer) => {
|
|
13
|
+
const parsed = JSON.parse(String(bodyBuffer || Buffer.alloc(0)).toString("utf8"));
|
|
14
|
+
const encodedData = String(parsed?.message?.data || "");
|
|
15
|
+
const decodedData = encodedData
|
|
16
|
+
? JSON.parse(Buffer.from(encodedData, "base64").toString("utf8"))
|
|
17
|
+
: {};
|
|
18
|
+
return {
|
|
19
|
+
envelope: parsed || {},
|
|
20
|
+
payload: decodedData || {},
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const proxyPushToServe = async ({
|
|
25
|
+
port,
|
|
26
|
+
bodyBuffer,
|
|
27
|
+
headers,
|
|
28
|
+
}) =>
|
|
29
|
+
await new Promise((resolve, reject) => {
|
|
30
|
+
const request = http.request(
|
|
31
|
+
{
|
|
32
|
+
hostname: "127.0.0.1",
|
|
33
|
+
port,
|
|
34
|
+
method: "POST",
|
|
35
|
+
path: "/",
|
|
36
|
+
headers: {
|
|
37
|
+
"content-type": headers["content-type"] || "application/json",
|
|
38
|
+
"content-length": String(bodyBuffer.length),
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
(response) => {
|
|
42
|
+
const chunks = [];
|
|
43
|
+
response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
44
|
+
response.on("end", () => {
|
|
45
|
+
resolve({
|
|
46
|
+
statusCode: response.statusCode || 200,
|
|
47
|
+
body: Buffer.concat(chunks).toString("utf8"),
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
request.on("error", reject);
|
|
53
|
+
if (bodyBuffer.length) request.write(bodyBuffer);
|
|
54
|
+
request.end();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const createGmailPushHandler = ({
|
|
58
|
+
resolvePushToken,
|
|
59
|
+
resolveTargetByEmail,
|
|
60
|
+
markPushReceived,
|
|
61
|
+
}) =>
|
|
62
|
+
async (req, res) => {
|
|
63
|
+
try {
|
|
64
|
+
const expectedToken = String(resolvePushToken?.() || "").trim();
|
|
65
|
+
const receivedToken = String(req.query?.token || "").trim();
|
|
66
|
+
if (!expectedToken || !receivedToken || expectedToken !== receivedToken) {
|
|
67
|
+
return res.status(401).json({ ok: false, error: "Invalid push token" });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const bodyBuffer = extractBodyBuffer(req.body);
|
|
71
|
+
const { payload } = parsePushEnvelope(bodyBuffer);
|
|
72
|
+
const email = String(payload?.emailAddress || "").trim().toLowerCase();
|
|
73
|
+
if (!email) {
|
|
74
|
+
return res.status(200).json({ ok: true, ignored: true, reason: "missing_email" });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const target = resolveTargetByEmail?.(email);
|
|
78
|
+
if (!target?.port) {
|
|
79
|
+
return res.status(200).json({ ok: true, ignored: true, reason: "watch_not_enabled" });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const proxied = await proxyPushToServe({
|
|
84
|
+
port: target.port,
|
|
85
|
+
bodyBuffer,
|
|
86
|
+
headers: req.headers || {},
|
|
87
|
+
});
|
|
88
|
+
await markPushReceived?.({
|
|
89
|
+
accountId: target.accountId,
|
|
90
|
+
at: Date.now(),
|
|
91
|
+
});
|
|
92
|
+
return res
|
|
93
|
+
.status(proxied.statusCode)
|
|
94
|
+
.send(proxied.body || "");
|
|
95
|
+
} catch (err) {
|
|
96
|
+
console.error(
|
|
97
|
+
`[alphaclaw] Gmail push proxy error for ${email}: ${err.message || "unknown"}`,
|
|
98
|
+
);
|
|
99
|
+
return res.status(200).json({ ok: true, ignored: true, reason: "proxy_error" });
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error("[alphaclaw] Gmail push handler error:", err);
|
|
103
|
+
return res.status(200).json({ ok: true, ignored: true, reason: "handler_error" });
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
createGmailPushHandler,
|
|
109
|
+
};
|