@chrysb/alphaclaw 0.3.5-beta.1 → 0.4.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/js/app.js +126 -105
- 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/index.js +44 -6
- package/lib/public/js/components/file-viewer/status-banners.js +11 -6
- package/lib/public/js/components/file-viewer/toolbar.js +43 -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 +94 -2
- package/lib/public/js/components/google/account-row.js +98 -0
- package/lib/public/js/components/google/add-account-modal.js +93 -0
- package/lib/public/js/components/google/index.js +439 -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/sidebar-git-panel.js +43 -14
- package/lib/public/js/components/sidebar.js +91 -75
- package/lib/public/js/lib/api.js +72 -8
- package/lib/public/js/lib/browse-file-policies.js +29 -11
- 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 +19 -7
- package/lib/server/google-state.js +187 -0
- package/lib/server/helpers.js +12 -4
- 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/google.js +414 -213
- package/lib/setup/gitignore +3 -0
- package/lib/setup/hourly-git-sync.sh +28 -1
- package/package.json +1 -1
- package/lib/public/js/components/google.js +0 -228
|
@@ -172,83 +172,99 @@ export const AppSidebar = ({
|
|
|
172
172
|
<${FolderLineIcon} className="sidebar-tab-icon" />
|
|
173
173
|
</button>
|
|
174
174
|
</div>
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
175
|
+
<div
|
|
176
|
+
style=${{
|
|
177
|
+
display: sidebarTab === "menu" ? "flex" : "none",
|
|
178
|
+
flexDirection: "column",
|
|
179
|
+
flex: "1 1 auto",
|
|
180
|
+
minHeight: 0,
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
${navSections.map(
|
|
184
|
+
(section) => html`
|
|
185
|
+
<div class="sidebar-label">${section.label}</div>
|
|
186
|
+
<nav class="sidebar-nav">
|
|
187
|
+
${section.items.map(
|
|
188
|
+
(item) => html`
|
|
189
|
+
<a
|
|
190
|
+
class=${selectedNavId === item.id ? "active" : ""}
|
|
191
|
+
onclick=${() => onSelectNavItem(item.id)}
|
|
192
|
+
>
|
|
193
|
+
${item.label}
|
|
194
|
+
</a>
|
|
195
|
+
`,
|
|
196
|
+
)}
|
|
197
|
+
</nav>
|
|
198
|
+
`,
|
|
199
|
+
)}
|
|
200
|
+
<div class="sidebar-footer">
|
|
201
|
+
${acHasUpdate && acLatest && !acDismissed
|
|
202
|
+
? html`
|
|
203
|
+
<${UpdateActionButton}
|
|
204
|
+
onClick=${onAcUpdate}
|
|
205
|
+
loading=${acUpdating}
|
|
206
|
+
warning=${true}
|
|
207
|
+
idleLabel=${`Update to v${acLatest}`}
|
|
208
|
+
loadingLabel="Updating..."
|
|
209
|
+
className="w-full justify-center"
|
|
202
210
|
/>
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
211
|
+
`
|
|
212
|
+
: null}
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
<div
|
|
216
|
+
style=${{
|
|
217
|
+
display: sidebarTab === "browse" ? "flex" : "none",
|
|
218
|
+
flexDirection: "column",
|
|
219
|
+
flex: "1 1 auto",
|
|
220
|
+
minHeight: 0,
|
|
221
|
+
overflow: "hidden",
|
|
222
|
+
}}
|
|
223
|
+
>
|
|
224
|
+
<div class="sidebar-browse-layout" ref=${browseLayoutRef}>
|
|
225
|
+
<div
|
|
226
|
+
class="sidebar-browse-panel"
|
|
227
|
+
>
|
|
228
|
+
<${FileTree}
|
|
229
|
+
onSelectFile=${onSelectBrowseFile}
|
|
230
|
+
selectedPath=${selectedBrowsePath}
|
|
231
|
+
onPreviewFile=${onPreviewBrowseFile}
|
|
232
|
+
isActive=${sidebarTab === "browse"}
|
|
233
|
+
/>
|
|
234
|
+
</div>
|
|
235
|
+
<div
|
|
236
|
+
class=${`sidebar-browse-resizer ${isResizingBrowsePanels ? "is-resizing" : ""}`}
|
|
237
|
+
onpointerdown=${onBrowsePanelResizerPointerDown}
|
|
238
|
+
role="separator"
|
|
239
|
+
aria-orientation="horizontal"
|
|
240
|
+
aria-label="Resize browse and git panels"
|
|
241
|
+
></div>
|
|
242
|
+
<div class="sidebar-browse-bottom">
|
|
243
|
+
<div
|
|
244
|
+
class="sidebar-browse-bottom-inner"
|
|
245
|
+
ref=${browseBottomPanelRef}
|
|
246
|
+
style=${{ height: `${browseBottomPanelHeightPx}px` }}
|
|
247
|
+
>
|
|
248
|
+
<${SidebarGitPanel}
|
|
249
|
+
onSelectFile=${onSelectBrowseFile}
|
|
250
|
+
isActive=${sidebarTab === "browse"}
|
|
251
|
+
/>
|
|
252
|
+
${acHasUpdate && acLatest && !acDismissed
|
|
253
|
+
? html`
|
|
254
|
+
<${UpdateActionButton}
|
|
255
|
+
onClick=${onAcUpdate}
|
|
256
|
+
loading=${acUpdating}
|
|
257
|
+
warning=${true}
|
|
258
|
+
idleLabel=${`Update to v${acLatest}`}
|
|
259
|
+
loadingLabel="Updating..."
|
|
260
|
+
className="w-full justify-center"
|
|
261
|
+
/>
|
|
262
|
+
`
|
|
263
|
+
: null}
|
|
249
264
|
</div>
|
|
250
|
-
|
|
251
|
-
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
252
268
|
</div>
|
|
253
269
|
`;
|
|
254
270
|
};
|
package/lib/public/js/lib/api.js
CHANGED
|
@@ -38,27 +38,73 @@ export async function rejectPairing(id, channel) {
|
|
|
38
38
|
return res.json();
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
export async function
|
|
42
|
-
const res = await authFetch('/api/google/
|
|
41
|
+
export async function fetchGoogleAccounts() {
|
|
42
|
+
const res = await authFetch('/api/google/accounts');
|
|
43
43
|
return res.json();
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
export async function
|
|
47
|
-
const
|
|
46
|
+
export async function fetchGoogleStatus(accountId = "") {
|
|
47
|
+
const params = new URLSearchParams();
|
|
48
|
+
if (accountId) params.set("accountId", String(accountId));
|
|
49
|
+
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
50
|
+
const res = await authFetch(`/api/google/status${suffix}`);
|
|
48
51
|
return res.json();
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
export async function
|
|
54
|
+
export async function checkGoogleApis(accountId = "") {
|
|
55
|
+
const params = new URLSearchParams();
|
|
56
|
+
if (accountId) params.set("accountId", String(accountId));
|
|
57
|
+
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
58
|
+
const res = await authFetch(`/api/google/check${suffix}`);
|
|
59
|
+
return res.json();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function saveGoogleCredentials({
|
|
63
|
+
clientId,
|
|
64
|
+
clientSecret,
|
|
65
|
+
email,
|
|
66
|
+
services = [],
|
|
67
|
+
client = "default",
|
|
68
|
+
personal = false,
|
|
69
|
+
accountId = "",
|
|
70
|
+
}) {
|
|
52
71
|
const res = await authFetch('/api/google/credentials', {
|
|
53
72
|
method: 'POST',
|
|
54
73
|
headers: { 'Content-Type': 'application/json' },
|
|
55
|
-
body: JSON.stringify({
|
|
74
|
+
body: JSON.stringify({
|
|
75
|
+
clientId,
|
|
76
|
+
clientSecret,
|
|
77
|
+
email,
|
|
78
|
+
services,
|
|
79
|
+
client,
|
|
80
|
+
personal,
|
|
81
|
+
accountId,
|
|
82
|
+
}),
|
|
56
83
|
});
|
|
57
84
|
return res.json();
|
|
58
85
|
}
|
|
59
86
|
|
|
60
|
-
export async function
|
|
61
|
-
|
|
87
|
+
export async function saveGoogleAccount({
|
|
88
|
+
email,
|
|
89
|
+
services = [],
|
|
90
|
+
client = "default",
|
|
91
|
+
personal = false,
|
|
92
|
+
accountId = "",
|
|
93
|
+
}) {
|
|
94
|
+
const res = await authFetch('/api/google/accounts', {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: { 'Content-Type': 'application/json' },
|
|
97
|
+
body: JSON.stringify({ email, services, client, personal, accountId }),
|
|
98
|
+
});
|
|
99
|
+
return res.json();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function disconnectGoogle(accountId = "") {
|
|
103
|
+
const res = await authFetch('/api/google/disconnect', {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: { 'Content-Type': 'application/json' },
|
|
106
|
+
body: JSON.stringify({ accountId }),
|
|
107
|
+
});
|
|
62
108
|
return res.json();
|
|
63
109
|
}
|
|
64
110
|
|
|
@@ -389,6 +435,24 @@ export const saveFileContent = async (filePath, content) => {
|
|
|
389
435
|
return parseJsonOrThrow(res, 'Could not save file');
|
|
390
436
|
};
|
|
391
437
|
|
|
438
|
+
export const deleteBrowseFile = async (filePath) => {
|
|
439
|
+
const res = await authFetch('/api/browse/delete', {
|
|
440
|
+
method: 'DELETE',
|
|
441
|
+
headers: { 'Content-Type': 'application/json' },
|
|
442
|
+
body: JSON.stringify({ path: String(filePath || '') }),
|
|
443
|
+
});
|
|
444
|
+
return parseJsonOrThrow(res, 'Could not delete file');
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
export const restoreBrowseFile = async (filePath) => {
|
|
448
|
+
const res = await authFetch('/api/browse/restore', {
|
|
449
|
+
method: 'POST',
|
|
450
|
+
headers: { 'Content-Type': 'application/json' },
|
|
451
|
+
body: JSON.stringify({ path: String(filePath || '') }),
|
|
452
|
+
});
|
|
453
|
+
return parseJsonOrThrow(res, 'Could not restore file');
|
|
454
|
+
};
|
|
455
|
+
|
|
392
456
|
export const fetchBrowseGitSummary = async () => {
|
|
393
457
|
const res = await authFetch('/api/browse/git-summary');
|
|
394
458
|
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 || "")
|
|
@@ -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);
|
|
@@ -258,6 +259,11 @@ const GOG_CONFIG_DIR = path.join(OPENCLAW_DIR, "gogcli");
|
|
|
258
259
|
const GOG_CREDENTIALS_PATH = path.join(GOG_CONFIG_DIR, "credentials.json");
|
|
259
260
|
const GOG_STATE_PATH = path.join(GOG_CONFIG_DIR, "state.json");
|
|
260
261
|
const GOG_KEYRING_PASSWORD = process.env.GOG_KEYRING_PASSWORD || "alphaclaw";
|
|
262
|
+
const kMaxGoogleAccounts = 5;
|
|
263
|
+
const gogClientCredentialsPath = (clientName = "default") =>
|
|
264
|
+
clientName === "default"
|
|
265
|
+
? GOG_CREDENTIALS_PATH
|
|
266
|
+
: path.join(GOG_CONFIG_DIR, `credentials-${clientName}.json`);
|
|
261
267
|
|
|
262
268
|
const API_TEST_COMMANDS = {
|
|
263
269
|
gmail: "gmail labels list",
|
|
@@ -274,13 +280,16 @@ const kChannelDefs = {
|
|
|
274
280
|
telegram: { envKey: "TELEGRAM_BOT_TOKEN" },
|
|
275
281
|
discord: { envKey: "DISCORD_BOT_TOKEN" },
|
|
276
282
|
};
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
283
|
+
const kProtectedBrowsePaths = new Set(
|
|
284
|
+
Array.isArray(kBrowseFilePolicies?.protectedPaths)
|
|
285
|
+
? kBrowseFilePolicies.protectedPaths
|
|
286
|
+
: [],
|
|
287
|
+
);
|
|
288
|
+
const kLockedBrowsePaths = new Set(
|
|
289
|
+
Array.isArray(kBrowseFilePolicies?.lockedPaths)
|
|
290
|
+
? kBrowseFilePolicies.lockedPaths
|
|
291
|
+
: [],
|
|
292
|
+
);
|
|
284
293
|
|
|
285
294
|
const SETUP_API_PREFIXES = [
|
|
286
295
|
"/api/status",
|
|
@@ -350,6 +359,7 @@ module.exports = {
|
|
|
350
359
|
kSystemVars,
|
|
351
360
|
kKnownVars,
|
|
352
361
|
kKnownKeys,
|
|
362
|
+
kProtectedBrowsePaths,
|
|
353
363
|
kLockedBrowsePaths,
|
|
354
364
|
SCOPE_MAP,
|
|
355
365
|
REVERSE_SCOPE_MAP,
|
|
@@ -358,6 +368,8 @@ module.exports = {
|
|
|
358
368
|
GOG_CREDENTIALS_PATH,
|
|
359
369
|
GOG_STATE_PATH,
|
|
360
370
|
GOG_KEYRING_PASSWORD,
|
|
371
|
+
kMaxGoogleAccounts,
|
|
372
|
+
gogClientCredentialsPath,
|
|
361
373
|
API_TEST_COMMANDS,
|
|
362
374
|
kChannelDefs,
|
|
363
375
|
SETUP_API_PREFIXES,
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
|
|
3
|
+
const kGoogleStateVersion = 2;
|
|
4
|
+
const kDefaultGoogleClient = "default";
|
|
5
|
+
const kDefaultGoogleScopes = [
|
|
6
|
+
"gmail:read",
|
|
7
|
+
"calendar:read",
|
|
8
|
+
"calendar:write",
|
|
9
|
+
"drive:read",
|
|
10
|
+
"sheets:read",
|
|
11
|
+
"docs:read",
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const createEmptyGoogleState = () => ({
|
|
15
|
+
version: kGoogleStateVersion,
|
|
16
|
+
accounts: [],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const createGoogleAccountId = () => crypto.randomBytes(4).toString("hex");
|
|
20
|
+
|
|
21
|
+
const normalizeScopes = (services) => {
|
|
22
|
+
if (!Array.isArray(services)) return [...kDefaultGoogleScopes];
|
|
23
|
+
const deduped = Array.from(
|
|
24
|
+
new Set(
|
|
25
|
+
services
|
|
26
|
+
.map((scope) => String(scope || "").trim())
|
|
27
|
+
.filter(Boolean),
|
|
28
|
+
),
|
|
29
|
+
);
|
|
30
|
+
return deduped.length ? deduped : [...kDefaultGoogleScopes];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const isLikelyPersonalEmail = (email = "") => {
|
|
34
|
+
const normalized = String(email || "").trim().toLowerCase();
|
|
35
|
+
return normalized.endsWith("@gmail.com") || normalized.endsWith("@googlemail.com");
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const normalizePersonalFlag = ({ account = {}, client = kDefaultGoogleClient }) => {
|
|
39
|
+
if (typeof account.personal === "boolean") return account.personal;
|
|
40
|
+
if (client === "personal") return true;
|
|
41
|
+
return isLikelyPersonalEmail(account.email);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const normalizeGoogleAccount = (account = {}) => ({
|
|
45
|
+
// Backward-compatible migration path for older state entries that predate
|
|
46
|
+
// explicit personal flags or were saved before the personal marker existed.
|
|
47
|
+
...(() => {
|
|
48
|
+
const client =
|
|
49
|
+
String(account.client || kDefaultGoogleClient).trim() || kDefaultGoogleClient;
|
|
50
|
+
return {
|
|
51
|
+
id: String(account.id || createGoogleAccountId()),
|
|
52
|
+
email: String(account.email || "").trim(),
|
|
53
|
+
client,
|
|
54
|
+
personal: normalizePersonalFlag({ account, client }),
|
|
55
|
+
services: normalizeScopes(account.services),
|
|
56
|
+
authenticated: Boolean(account.authenticated),
|
|
57
|
+
};
|
|
58
|
+
})(),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const normalizeGoogleStateV2 = (state = {}) => {
|
|
62
|
+
const accounts = Array.isArray(state.accounts)
|
|
63
|
+
? state.accounts.map((account) => normalizeGoogleAccount(account))
|
|
64
|
+
: [];
|
|
65
|
+
return {
|
|
66
|
+
version: kGoogleStateVersion,
|
|
67
|
+
accounts,
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const hasPersonalGoogleAccount = (state = {}) =>
|
|
72
|
+
(state.accounts || []).some((account) => account.personal);
|
|
73
|
+
|
|
74
|
+
const writeGoogleState = ({ fs, statePath, state }) => {
|
|
75
|
+
const normalized = normalizeGoogleStateV2(state);
|
|
76
|
+
fs.writeFileSync(statePath, JSON.stringify(normalized, null, 2));
|
|
77
|
+
return normalized;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const migrateGoogleStateV1 = ({ fs, statePath, rawState = {} }) => {
|
|
81
|
+
const email = String(rawState.email || "").trim();
|
|
82
|
+
const accounts = email
|
|
83
|
+
? [
|
|
84
|
+
normalizeGoogleAccount({
|
|
85
|
+
id: createGoogleAccountId(),
|
|
86
|
+
email,
|
|
87
|
+
services: rawState.services,
|
|
88
|
+
authenticated: Boolean(rawState.authenticated),
|
|
89
|
+
client: kDefaultGoogleClient,
|
|
90
|
+
personal: false,
|
|
91
|
+
}),
|
|
92
|
+
]
|
|
93
|
+
: [];
|
|
94
|
+
const migrated = {
|
|
95
|
+
version: kGoogleStateVersion,
|
|
96
|
+
accounts,
|
|
97
|
+
};
|
|
98
|
+
fs.writeFileSync(statePath, JSON.stringify(migrated, null, 2));
|
|
99
|
+
return migrated;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const readGoogleState = ({ fs, statePath }) => {
|
|
103
|
+
if (!fs.existsSync(statePath)) return createEmptyGoogleState();
|
|
104
|
+
try {
|
|
105
|
+
const raw = JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
106
|
+
if (raw && raw.version === kGoogleStateVersion && Array.isArray(raw.accounts)) {
|
|
107
|
+
const normalized = normalizeGoogleStateV2(raw);
|
|
108
|
+
if (JSON.stringify(raw) !== JSON.stringify(normalized)) {
|
|
109
|
+
fs.writeFileSync(statePath, JSON.stringify(normalized, null, 2));
|
|
110
|
+
}
|
|
111
|
+
return normalized;
|
|
112
|
+
}
|
|
113
|
+
return migrateGoogleStateV1({ fs, statePath, rawState: raw || {} });
|
|
114
|
+
} catch {
|
|
115
|
+
return createEmptyGoogleState();
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const listGoogleAccounts = (state = {}) => [...(state.accounts || [])];
|
|
120
|
+
|
|
121
|
+
const getGoogleAccountById = (state = {}, accountId = "") =>
|
|
122
|
+
(state.accounts || []).find((account) => account.id === accountId) || null;
|
|
123
|
+
|
|
124
|
+
const getGoogleAccountByEmailAndClient = (
|
|
125
|
+
state = {},
|
|
126
|
+
email = "",
|
|
127
|
+
client = kDefaultGoogleClient,
|
|
128
|
+
) =>
|
|
129
|
+
(state.accounts || []).find(
|
|
130
|
+
(account) => account.email === email && account.client === client,
|
|
131
|
+
) || null;
|
|
132
|
+
|
|
133
|
+
const upsertGoogleAccount = ({
|
|
134
|
+
state,
|
|
135
|
+
account,
|
|
136
|
+
maxAccounts = 5,
|
|
137
|
+
}) => {
|
|
138
|
+
const nextState = normalizeGoogleStateV2(state);
|
|
139
|
+
const normalized = normalizeGoogleAccount(account);
|
|
140
|
+
if (!normalized.email) throw new Error("Account email is required");
|
|
141
|
+
const existingIdx = nextState.accounts.findIndex((item) => item.id === normalized.id);
|
|
142
|
+
|
|
143
|
+
if (normalized.personal) {
|
|
144
|
+
const personalExists = nextState.accounts.some(
|
|
145
|
+
(item, idx) => item.personal && idx !== existingIdx,
|
|
146
|
+
);
|
|
147
|
+
if (personalExists) {
|
|
148
|
+
throw new Error("Only one personal account is allowed");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (existingIdx >= 0) {
|
|
153
|
+
nextState.accounts[existingIdx] = normalized;
|
|
154
|
+
return { state: nextState, account: normalized };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (nextState.accounts.length >= maxAccounts) {
|
|
158
|
+
throw new Error(`Maximum ${maxAccounts} Google accounts allowed`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
nextState.accounts.push(normalized);
|
|
162
|
+
return { state: nextState, account: normalized };
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const removeGoogleAccount = ({ state, accountId }) => {
|
|
166
|
+
const nextState = normalizeGoogleStateV2(state);
|
|
167
|
+
const removed = getGoogleAccountById(nextState, accountId);
|
|
168
|
+
if (!removed) return { state: nextState, account: null };
|
|
169
|
+
nextState.accounts = nextState.accounts.filter((account) => account.id !== accountId);
|
|
170
|
+
return { state: nextState, account: removed };
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
module.exports = {
|
|
174
|
+
kGoogleStateVersion,
|
|
175
|
+
kDefaultGoogleClient,
|
|
176
|
+
kDefaultGoogleScopes,
|
|
177
|
+
createGoogleAccountId,
|
|
178
|
+
createEmptyGoogleState,
|
|
179
|
+
readGoogleState,
|
|
180
|
+
writeGoogleState,
|
|
181
|
+
listGoogleAccounts,
|
|
182
|
+
getGoogleAccountById,
|
|
183
|
+
getGoogleAccountByEmailAndClient,
|
|
184
|
+
upsertGoogleAccount,
|
|
185
|
+
removeGoogleAccount,
|
|
186
|
+
hasPersonalGoogleAccount,
|
|
187
|
+
};
|
package/lib/server/helpers.js
CHANGED
|
@@ -3,7 +3,7 @@ const crypto = require("crypto");
|
|
|
3
3
|
const {
|
|
4
4
|
CODEX_JWT_CLAIM_PATH,
|
|
5
5
|
kOnboardingModelProviders,
|
|
6
|
-
|
|
6
|
+
gogClientCredentialsPath,
|
|
7
7
|
} = require("./constants");
|
|
8
8
|
|
|
9
9
|
const normalizeOpenclawVersion = (rawVersion) => {
|
|
@@ -170,9 +170,10 @@ const getApiEnableUrl = (svc, projectId) => {
|
|
|
170
170
|
return `https://console.developers.google.com/apis/api/${api}/overview${project}`;
|
|
171
171
|
};
|
|
172
172
|
|
|
173
|
-
const readGoogleCredentials = () => {
|
|
173
|
+
const readGoogleCredentials = (clientName = "default") => {
|
|
174
174
|
try {
|
|
175
|
-
const
|
|
175
|
+
const credentialsPath = gogClientCredentialsPath(clientName);
|
|
176
|
+
const c = JSON.parse(fs.readFileSync(credentialsPath, "utf8"));
|
|
176
177
|
return {
|
|
177
178
|
clientId:
|
|
178
179
|
c.web?.client_id || c.installed?.client_id || c.client_id || null,
|
|
@@ -181,9 +182,16 @@ const readGoogleCredentials = () => {
|
|
|
181
182
|
c.installed?.client_secret ||
|
|
182
183
|
c.client_secret ||
|
|
183
184
|
null,
|
|
185
|
+
path: credentialsPath,
|
|
186
|
+
client: clientName,
|
|
184
187
|
};
|
|
185
188
|
} catch {
|
|
186
|
-
return {
|
|
189
|
+
return {
|
|
190
|
+
clientId: null,
|
|
191
|
+
clientSecret: null,
|
|
192
|
+
path: gogClientCredentialsPath(clientName),
|
|
193
|
+
client: clientName,
|
|
194
|
+
};
|
|
187
195
|
}
|
|
188
196
|
};
|
|
189
197
|
|