@chrysb/alphaclaw 0.8.4 → 0.8.6
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/explorer.css +48 -0
- package/lib/public/css/shell.css +149 -0
- package/lib/public/css/tailwind.generated.css +1 -1
- package/lib/public/css/theme.css +265 -0
- package/lib/public/dist/app.bundle.js +2269 -2200
- package/lib/public/js/app.js +4 -0
- package/lib/public/js/components/icons.js +38 -0
- package/lib/public/js/components/models-tab/provider-auth-card.js +60 -49
- package/lib/public/js/components/models-tab/use-models.js +74 -9
- package/lib/public/js/components/models.js +52 -37
- package/lib/public/js/components/onboarding/use-welcome-codex.js +34 -24
- package/lib/public/js/components/onboarding/welcome-config.js +76 -10
- package/lib/public/js/components/onboarding/welcome-form-step.js +2 -7
- package/lib/public/js/components/onboarding/welcome-header.js +12 -14
- package/lib/public/js/components/onboarding/welcome-setup-step.js +3 -3
- package/lib/public/js/components/providers.js +53 -42
- package/lib/public/js/components/sidebar.js +9 -1
- package/lib/public/js/components/theme-toggle.js +113 -0
- package/lib/public/js/components/welcome/index.js +0 -2
- package/lib/public/js/components/welcome/use-welcome.js +101 -36
- package/lib/public/js/lib/codex-oauth-window.js +22 -0
- package/lib/public/js/lib/model-catalog.js +20 -0
- package/lib/public/js/lib/storage-keys.js +1 -1
- package/lib/public/login.html +8 -4
- package/lib/public/setup.html +9 -0
- package/lib/server/alphaclaw-version.js +60 -13
- package/lib/server/db/webhooks/index.js +48 -8
- package/lib/server/model-catalog-cache.js +251 -0
- package/lib/server/routes/models.js +14 -23
- package/lib/server/routes/webhooks.js +12 -1
- package/package.json +1 -1
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { ALPHACLAW_DIR, kFallbackOnboardingModels } = require("./constants");
|
|
4
|
+
|
|
5
|
+
const kModelCatalogCacheVersion = 1;
|
|
6
|
+
const kModelCatalogRefreshBackoffMs = 30 * 1000;
|
|
7
|
+
const kDefaultCachePath = path.join(ALPHACLAW_DIR, "cache", "model-catalog.json");
|
|
8
|
+
|
|
9
|
+
const createResponse = ({
|
|
10
|
+
source = "fallback",
|
|
11
|
+
fetchedAt = null,
|
|
12
|
+
stale = false,
|
|
13
|
+
refreshing = false,
|
|
14
|
+
models = [],
|
|
15
|
+
} = {}) => ({
|
|
16
|
+
ok: true,
|
|
17
|
+
source,
|
|
18
|
+
fetchedAt,
|
|
19
|
+
stale,
|
|
20
|
+
refreshing,
|
|
21
|
+
models,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const normalizeCachedModels = ({
|
|
25
|
+
models,
|
|
26
|
+
normalizeOnboardingModels = (items) => items,
|
|
27
|
+
} = {}) =>
|
|
28
|
+
normalizeOnboardingModels(
|
|
29
|
+
(Array.isArray(models) ? models : []).map((model) => ({
|
|
30
|
+
key: model?.key,
|
|
31
|
+
name: model?.label || model?.name || model?.key,
|
|
32
|
+
})),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const normalizeCacheEntry = ({
|
|
36
|
+
raw,
|
|
37
|
+
normalizeOnboardingModels = (items) => items,
|
|
38
|
+
} = {}) => {
|
|
39
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
40
|
+
const fetchedAt = Number(raw.fetchedAt || 0);
|
|
41
|
+
const models = normalizeCachedModels({
|
|
42
|
+
models: raw.models,
|
|
43
|
+
normalizeOnboardingModels,
|
|
44
|
+
});
|
|
45
|
+
if (!Number.isFinite(fetchedAt) || fetchedAt <= 0 || models.length === 0) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
version: kModelCatalogCacheVersion,
|
|
50
|
+
fetchedAt,
|
|
51
|
+
models,
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const createModelCatalogCache = ({
|
|
56
|
+
fsModule = fs,
|
|
57
|
+
pathModule = path,
|
|
58
|
+
shellCmd,
|
|
59
|
+
gatewayEnv = () => ({}),
|
|
60
|
+
parseJsonFromNoisyOutput = () => ({}),
|
|
61
|
+
normalizeOnboardingModels = (items) => items,
|
|
62
|
+
fallbackModels = kFallbackOnboardingModels,
|
|
63
|
+
cachePath = kDefaultCachePath,
|
|
64
|
+
refreshBackoffMs = kModelCatalogRefreshBackoffMs,
|
|
65
|
+
now = () => Date.now(),
|
|
66
|
+
setTimeoutFn = setTimeout,
|
|
67
|
+
clearTimeoutFn = clearTimeout,
|
|
68
|
+
logger = console,
|
|
69
|
+
} = {}) => {
|
|
70
|
+
let cacheLoaded = false;
|
|
71
|
+
let memoryCache = null;
|
|
72
|
+
let cacheIsStale = false;
|
|
73
|
+
let refreshPromise = null;
|
|
74
|
+
let retryTimer = null;
|
|
75
|
+
let backoffUntilMs = 0;
|
|
76
|
+
|
|
77
|
+
const clearRetryTimer = () => {
|
|
78
|
+
if (!retryTimer) return;
|
|
79
|
+
clearTimeoutFn(retryTimer);
|
|
80
|
+
retryTimer = null;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const isRefreshPending = () => !!refreshPromise || !!retryTimer;
|
|
84
|
+
|
|
85
|
+
const setCacheEntry = (entry, { fresh = false } = {}) => {
|
|
86
|
+
memoryCache = entry;
|
|
87
|
+
cacheLoaded = true;
|
|
88
|
+
cacheIsStale = !fresh;
|
|
89
|
+
backoffUntilMs = 0;
|
|
90
|
+
clearRetryTimer();
|
|
91
|
+
return memoryCache;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const readDiskCache = () => {
|
|
95
|
+
if (cacheLoaded) return memoryCache;
|
|
96
|
+
cacheLoaded = true;
|
|
97
|
+
try {
|
|
98
|
+
const raw = JSON.parse(fsModule.readFileSync(cachePath, "utf8"));
|
|
99
|
+
const entry = normalizeCacheEntry({
|
|
100
|
+
raw,
|
|
101
|
+
normalizeOnboardingModels,
|
|
102
|
+
});
|
|
103
|
+
if (!entry) return null;
|
|
104
|
+
memoryCache = entry;
|
|
105
|
+
cacheIsStale = true;
|
|
106
|
+
return memoryCache;
|
|
107
|
+
} catch {
|
|
108
|
+
memoryCache = null;
|
|
109
|
+
cacheIsStale = false;
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const writeDiskCache = (entry) => {
|
|
115
|
+
fsModule.mkdirSync(pathModule.dirname(cachePath), { recursive: true });
|
|
116
|
+
fsModule.writeFileSync(
|
|
117
|
+
cachePath,
|
|
118
|
+
`${JSON.stringify(entry, null, 2)}\n`,
|
|
119
|
+
"utf8",
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const loadFreshCatalog = async () => {
|
|
124
|
+
const output = await shellCmd("openclaw models list --all --json", {
|
|
125
|
+
env: gatewayEnv(),
|
|
126
|
+
timeout: 20000,
|
|
127
|
+
});
|
|
128
|
+
const parsed = parseJsonFromNoisyOutput(output);
|
|
129
|
+
const models = normalizeOnboardingModels(parsed?.models || []);
|
|
130
|
+
if (models.length === 0) {
|
|
131
|
+
throw new Error("No models found");
|
|
132
|
+
}
|
|
133
|
+
const entry = {
|
|
134
|
+
version: kModelCatalogCacheVersion,
|
|
135
|
+
fetchedAt: now(),
|
|
136
|
+
models,
|
|
137
|
+
};
|
|
138
|
+
writeDiskCache(entry);
|
|
139
|
+
setCacheEntry(entry, { fresh: true });
|
|
140
|
+
return entry;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const scheduleRetry = () => {
|
|
144
|
+
if (!memoryCache || retryTimer) return;
|
|
145
|
+
const delayMs = Math.max(backoffUntilMs - now(), 0);
|
|
146
|
+
retryTimer = setTimeoutFn(() => {
|
|
147
|
+
retryTimer = null;
|
|
148
|
+
if (!memoryCache || !cacheIsStale || refreshPromise) return;
|
|
149
|
+
void startBackgroundRefresh();
|
|
150
|
+
}, delayMs);
|
|
151
|
+
if (typeof retryTimer?.unref === "function") retryTimer.unref();
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const handleRefreshFailure = (err) => {
|
|
155
|
+
if (memoryCache) {
|
|
156
|
+
cacheIsStale = true;
|
|
157
|
+
backoffUntilMs = now() + refreshBackoffMs;
|
|
158
|
+
scheduleRetry();
|
|
159
|
+
logger.error?.(
|
|
160
|
+
`[models] Failed to refresh cached models: ${err.message || String(err)}`,
|
|
161
|
+
);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
logger.error?.(
|
|
165
|
+
`[models] Failed to load dynamic models: ${err.message || String(err)}`,
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const startBackgroundRefresh = () => {
|
|
170
|
+
readDiskCache();
|
|
171
|
+
if (!memoryCache) return null;
|
|
172
|
+
if (refreshPromise) return refreshPromise;
|
|
173
|
+
if (retryTimer) return null;
|
|
174
|
+
if (backoffUntilMs > now()) {
|
|
175
|
+
scheduleRetry();
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
refreshPromise = Promise.resolve()
|
|
179
|
+
.then(() => loadFreshCatalog())
|
|
180
|
+
.catch((err) => {
|
|
181
|
+
handleRefreshFailure(err);
|
|
182
|
+
return null;
|
|
183
|
+
})
|
|
184
|
+
.finally(() => {
|
|
185
|
+
refreshPromise = null;
|
|
186
|
+
});
|
|
187
|
+
return refreshPromise;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
async getCatalogResponse() {
|
|
192
|
+
readDiskCache();
|
|
193
|
+
if (memoryCache && !cacheIsStale) {
|
|
194
|
+
return createResponse({
|
|
195
|
+
source: "openclaw",
|
|
196
|
+
fetchedAt: memoryCache.fetchedAt,
|
|
197
|
+
stale: false,
|
|
198
|
+
refreshing: false,
|
|
199
|
+
models: memoryCache.models,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
if (memoryCache) {
|
|
203
|
+
startBackgroundRefresh();
|
|
204
|
+
return createResponse({
|
|
205
|
+
source: "cache",
|
|
206
|
+
fetchedAt: memoryCache.fetchedAt,
|
|
207
|
+
stale: true,
|
|
208
|
+
refreshing: isRefreshPending(),
|
|
209
|
+
models: memoryCache.models,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const freshEntry = await loadFreshCatalog();
|
|
214
|
+
return createResponse({
|
|
215
|
+
source: "openclaw",
|
|
216
|
+
fetchedAt: freshEntry.fetchedAt,
|
|
217
|
+
stale: false,
|
|
218
|
+
refreshing: false,
|
|
219
|
+
models: freshEntry.models,
|
|
220
|
+
});
|
|
221
|
+
} catch (err) {
|
|
222
|
+
handleRefreshFailure(err);
|
|
223
|
+
return createResponse({
|
|
224
|
+
source: "fallback",
|
|
225
|
+
fetchedAt: null,
|
|
226
|
+
stale: false,
|
|
227
|
+
refreshing: false,
|
|
228
|
+
models: fallbackModels,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
markStale() {
|
|
234
|
+
readDiskCache();
|
|
235
|
+
if (!memoryCache) return;
|
|
236
|
+
cacheIsStale = true;
|
|
237
|
+
backoffUntilMs = 0;
|
|
238
|
+
clearRetryTimer();
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
module.exports = {
|
|
244
|
+
createModelCatalogCache,
|
|
245
|
+
createResponse,
|
|
246
|
+
normalizeCachedModels,
|
|
247
|
+
normalizeCacheEntry,
|
|
248
|
+
kModelCatalogCacheVersion,
|
|
249
|
+
kModelCatalogRefreshBackoffMs,
|
|
250
|
+
kDefaultCachePath,
|
|
251
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { kFallbackOnboardingModels } = require("../constants");
|
|
2
|
+
const { createModelCatalogCache } = require("../model-catalog-cache");
|
|
2
3
|
|
|
3
4
|
const runModelsGitSync = async (shellCmd) => {
|
|
4
5
|
if (typeof shellCmd !== "function") return null;
|
|
@@ -22,6 +23,13 @@ const registerModelRoutes = ({
|
|
|
22
23
|
readEnvFile,
|
|
23
24
|
writeEnvFile,
|
|
24
25
|
reloadEnv,
|
|
26
|
+
modelCatalogCache = createModelCatalogCache({
|
|
27
|
+
shellCmd,
|
|
28
|
+
gatewayEnv,
|
|
29
|
+
parseJsonFromNoisyOutput,
|
|
30
|
+
normalizeOnboardingModels,
|
|
31
|
+
fallbackModels: kFallbackOnboardingModels,
|
|
32
|
+
}),
|
|
25
33
|
}) => {
|
|
26
34
|
const upsertEnvVar = (items, key, value) => {
|
|
27
35
|
const next = Array.isArray(items) ? [...items] : [];
|
|
@@ -154,29 +162,8 @@ const registerModelRoutes = ({
|
|
|
154
162
|
// ── Existing CLI-backed catalog/status routes ──
|
|
155
163
|
|
|
156
164
|
app.get("/api/models", async (req, res) => {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
env: gatewayEnv(),
|
|
160
|
-
timeout: 20000,
|
|
161
|
-
});
|
|
162
|
-
const parsed = parseJsonFromNoisyOutput(output);
|
|
163
|
-
const models = normalizeOnboardingModels(parsed?.models || []);
|
|
164
|
-
if (models.length > 0) {
|
|
165
|
-
return res.json({ ok: true, source: "openclaw", models });
|
|
166
|
-
}
|
|
167
|
-
return res.json({
|
|
168
|
-
ok: true,
|
|
169
|
-
source: "fallback",
|
|
170
|
-
models: kFallbackOnboardingModels,
|
|
171
|
-
});
|
|
172
|
-
} catch (err) {
|
|
173
|
-
console.error("[models] Failed to load dynamic models:", err.message);
|
|
174
|
-
return res.json({
|
|
175
|
-
ok: true,
|
|
176
|
-
source: "fallback",
|
|
177
|
-
models: kFallbackOnboardingModels,
|
|
178
|
-
});
|
|
179
|
-
}
|
|
165
|
+
const response = await modelCatalogCache.getCatalogResponse();
|
|
166
|
+
return res.json(response);
|
|
180
167
|
});
|
|
181
168
|
|
|
182
169
|
app.get("/api/models/status", async (req, res) => {
|
|
@@ -210,6 +197,7 @@ const registerModelRoutes = ({
|
|
|
210
197
|
env: gatewayEnv(),
|
|
211
198
|
timeout: 30000,
|
|
212
199
|
});
|
|
200
|
+
modelCatalogCache.markStale();
|
|
213
201
|
res.json({ ok: true });
|
|
214
202
|
} catch (err) {
|
|
215
203
|
res
|
|
@@ -286,6 +274,7 @@ const registerModelRoutes = ({
|
|
|
286
274
|
authProfiles.syncConfigAuthReferencesForAgent(agentId);
|
|
287
275
|
|
|
288
276
|
const syncWarning = await runModelsGitSync(shellCmd);
|
|
277
|
+
modelCatalogCache.markStale();
|
|
289
278
|
res.json({
|
|
290
279
|
ok: true,
|
|
291
280
|
...(syncWarning ? { syncWarning } : {}),
|
|
@@ -338,6 +327,7 @@ const registerModelRoutes = ({
|
|
|
338
327
|
const agentId = req.query.agentId || undefined;
|
|
339
328
|
authProfiles.upsertProfile(profileId, credential, agentId);
|
|
340
329
|
syncEnvVarsForProfiles([{ id: profileId, ...credential }]);
|
|
330
|
+
modelCatalogCache.markStale();
|
|
341
331
|
res.json({ ok: true });
|
|
342
332
|
} catch (err) {
|
|
343
333
|
res
|
|
@@ -359,6 +349,7 @@ const registerModelRoutes = ({
|
|
|
359
349
|
try {
|
|
360
350
|
const agentId = req.query.agentId || undefined;
|
|
361
351
|
const removed = authProfiles.removeProfile(profileId, agentId);
|
|
352
|
+
modelCatalogCache.markStale();
|
|
362
353
|
res.json({ ok: true, removed });
|
|
363
354
|
} catch (err) {
|
|
364
355
|
res
|
|
@@ -29,13 +29,24 @@ const mergeWebhookAndSummary = ({ webhook, summary }) => {
|
|
|
29
29
|
const totalCount = Number(summary?.totalCount || 0);
|
|
30
30
|
const errorCount = Number(summary?.errorCount || 0);
|
|
31
31
|
const successCount = Number(summary?.successCount || 0);
|
|
32
|
+
const recentTotalCount = Number(summary?.recentTotalCount || 0);
|
|
33
|
+
const recentErrorCount = Number(summary?.recentErrorCount || 0);
|
|
34
|
+
const recentSuccessCount = Number(summary?.recentSuccessCount || 0);
|
|
35
|
+
const healthWindowSize = Number(summary?.healthWindowSize || 0);
|
|
32
36
|
return {
|
|
33
37
|
...webhook,
|
|
34
38
|
lastReceived: summary?.lastReceived || null,
|
|
35
39
|
totalCount,
|
|
36
40
|
successCount,
|
|
37
41
|
errorCount,
|
|
38
|
-
|
|
42
|
+
recentTotalCount,
|
|
43
|
+
recentSuccessCount,
|
|
44
|
+
recentErrorCount,
|
|
45
|
+
healthWindowSize,
|
|
46
|
+
health: buildHealth({
|
|
47
|
+
totalCount: recentTotalCount || totalCount,
|
|
48
|
+
errorCount: recentTotalCount > 0 ? recentErrorCount : errorCount,
|
|
49
|
+
}),
|
|
39
50
|
};
|
|
40
51
|
};
|
|
41
52
|
|