@chrysb/alphaclaw 0.8.5 → 0.8.7-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 +56 -20
- 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 +2441 -2352
- package/lib/public/js/app.js +7 -0
- package/lib/public/js/components/gateway.js +6 -3
- package/lib/public/js/components/general/index.js +2 -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 +31 -11
- 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/routes/general-route.js +2 -0
- package/lib/public/js/components/routes/watchdog-route.js +2 -0
- package/lib/public/js/components/sidebar.js +29 -8
- package/lib/public/js/components/theme-toggle.js +113 -0
- package/lib/public/js/components/update-modal-helpers.js +12 -0
- package/lib/public/js/components/update-modal.js +2 -1
- package/lib/public/js/components/watchdog-tab/index.js +2 -0
- package/lib/public/js/components/welcome/index.js +1 -2
- package/lib/public/js/components/welcome/use-welcome.js +153 -38
- package/lib/public/js/hooks/use-app-shell-controller.js +33 -9
- package/lib/public/js/lib/api.js +35 -0
- 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 +30 -127
- package/lib/server/db/webhooks/index.js +48 -8
- package/lib/server/model-catalog-cache.js +251 -0
- package/lib/server/openclaw-version.js +59 -130
- package/lib/server/pending-alphaclaw-update.js +71 -0
- package/lib/server/pending-openclaw-update.js +71 -0
- package/lib/server/routes/models.js +14 -23
- package/lib/server/routes/system.js +6 -1
- package/lib/server/routes/webhooks.js +12 -1
- package/package.json +1 -1
package/lib/public/js/lib/api.js
CHANGED
|
@@ -527,6 +527,41 @@ export async function updateAlphaclaw() {
|
|
|
527
527
|
return res.json();
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
+
const delay = (ms) =>
|
|
531
|
+
new Promise((resolve) => {
|
|
532
|
+
setTimeout(resolve, Math.max(0, Number(ms) || 0));
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
export async function waitForAlphaclawRestart({
|
|
536
|
+
initialDelayMs = 1500,
|
|
537
|
+
intervalMs = 1000,
|
|
538
|
+
timeoutMs = 60000,
|
|
539
|
+
} = {}) {
|
|
540
|
+
const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0);
|
|
541
|
+
await delay(initialDelayMs);
|
|
542
|
+
|
|
543
|
+
while (Date.now() <= deadline) {
|
|
544
|
+
try {
|
|
545
|
+
const headers = new Headers();
|
|
546
|
+
const browserTimeZone = getBrowserTimeZone();
|
|
547
|
+
if (browserTimeZone) {
|
|
548
|
+
headers.set(kClientTimeZoneHeader, browserTimeZone);
|
|
549
|
+
}
|
|
550
|
+
const res = await fetch("/api/auth/status", {
|
|
551
|
+
cache: "no-store",
|
|
552
|
+
credentials: "same-origin",
|
|
553
|
+
headers,
|
|
554
|
+
});
|
|
555
|
+
if (res.status < 500) {
|
|
556
|
+
return { ok: true };
|
|
557
|
+
}
|
|
558
|
+
} catch {}
|
|
559
|
+
await delay(intervalMs);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
throw new Error("AlphaClaw restart is taking longer than expected");
|
|
563
|
+
}
|
|
564
|
+
|
|
530
565
|
export async function fetchSyncCron() {
|
|
531
566
|
const res = await authFetch("/api/sync-cron");
|
|
532
567
|
const text = await res.text();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const kCodexAuthStartPath = "/auth/codex/start";
|
|
2
|
+
const kCodexAuthWindowName = "codex-auth";
|
|
3
|
+
const kCodexAuthPopupFeatures = "popup=yes,width=640,height=780";
|
|
4
|
+
const kCodexAuthCallbackMessageType = "callback-input";
|
|
5
|
+
|
|
6
|
+
export const openCodexAuthWindow = () => {
|
|
7
|
+
const popup = window.open(
|
|
8
|
+
kCodexAuthStartPath,
|
|
9
|
+
kCodexAuthWindowName,
|
|
10
|
+
kCodexAuthPopupFeatures,
|
|
11
|
+
);
|
|
12
|
+
if (!popup || popup.closed) {
|
|
13
|
+
window.location.href = kCodexAuthStartPath;
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return popup;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const isCodexAuthCallbackMessage = (value) =>
|
|
20
|
+
value?.codex === kCodexAuthCallbackMessageType &&
|
|
21
|
+
typeof value.input === "string" &&
|
|
22
|
+
value.input.trim().length > 0;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getFeaturedModels } from "./model-config.js";
|
|
2
|
+
|
|
3
|
+
export const kModelCatalogCacheKey = "/api/models";
|
|
4
|
+
export const kModelCatalogPollIntervalMs = 3000;
|
|
5
|
+
|
|
6
|
+
export const getModelCatalogModels = (payload) =>
|
|
7
|
+
Array.isArray(payload?.models) ? payload.models : [];
|
|
8
|
+
|
|
9
|
+
export const isModelCatalogRefreshing = (payload) =>
|
|
10
|
+
Boolean(payload?.refreshing);
|
|
11
|
+
|
|
12
|
+
export const getInitialOnboardingModelKey = ({
|
|
13
|
+
catalog = [],
|
|
14
|
+
currentModelKey = "",
|
|
15
|
+
} = {}) => {
|
|
16
|
+
const normalizedCurrent = String(currentModelKey || "").trim();
|
|
17
|
+
if (normalizedCurrent) return normalizedCurrent;
|
|
18
|
+
const featuredModels = getFeaturedModels(catalog);
|
|
19
|
+
return String(featuredModels[0]?.key || catalog[0]?.key || "");
|
|
20
|
+
};
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
// --- UI settings (single JSON blob containing sub-keys) ---
|
|
8
8
|
export const kUiSettingsStorageKey = "alphaclaw.ui.settings";
|
|
9
|
+
export const kThemeStorageKey = "alphaclaw.ui.theme";
|
|
9
10
|
|
|
10
11
|
// --- Browse / file viewer ---
|
|
11
12
|
export const kFileViewerModeStorageKey = "alphaclaw.browse.viewerMode";
|
|
@@ -30,4 +31,3 @@ export const kAgentLastSessionKey = "alphaclaw.agent.lastSessionKey";
|
|
|
30
31
|
|
|
31
32
|
// --- Chat ---
|
|
32
33
|
export const kChatSessionDraftsStorageKey = "alphaclaw.chat.sessionDrafts";
|
|
33
|
-
|
package/lib/public/login.html
CHANGED
|
@@ -11,6 +11,14 @@
|
|
|
11
11
|
<link rel="icon" type="image/svg+xml" href="./img/logo.svg" />
|
|
12
12
|
<link rel="stylesheet" href="./css/theme.css" />
|
|
13
13
|
<link rel="stylesheet" href="./css/tailwind.generated.css" />
|
|
14
|
+
<script>
|
|
15
|
+
try {
|
|
16
|
+
var t = localStorage.getItem("alphaclaw.ui.theme");
|
|
17
|
+
if (t === "system") t = window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
|
|
18
|
+
else if (t !== "dark" && t !== "light") t = "dark";
|
|
19
|
+
document.documentElement.dataset.theme = t;
|
|
20
|
+
} catch {}
|
|
21
|
+
</script>
|
|
14
22
|
</head>
|
|
15
23
|
<body class="min-h-screen flex items-center justify-center p-4">
|
|
16
24
|
<div class="max-w-sm w-full relative z-10">
|
|
@@ -53,10 +61,6 @@
|
|
|
53
61
|
</form>
|
|
54
62
|
</div>
|
|
55
63
|
<script>
|
|
56
|
-
try {
|
|
57
|
-
window.localStorage?.clear?.();
|
|
58
|
-
} catch {}
|
|
59
|
-
|
|
60
64
|
const formEl = document.getElementById("login-form");
|
|
61
65
|
const passwordEl = document.getElementById("password");
|
|
62
66
|
const submitButtonEl = document.getElementById("submit-btn");
|
package/lib/public/setup.html
CHANGED
|
@@ -16,6 +16,15 @@
|
|
|
16
16
|
<link rel="stylesheet" href="./css/agents.css" />
|
|
17
17
|
<link rel="stylesheet" href="./css/chat.css" />
|
|
18
18
|
<link rel="stylesheet" href="./css/cron.css" />
|
|
19
|
+
<script>
|
|
20
|
+
// Apply saved theme before render to prevent flash.
|
|
21
|
+
try {
|
|
22
|
+
var t = localStorage.getItem("alphaclaw.ui.theme");
|
|
23
|
+
if (t === "system") t = window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
|
|
24
|
+
else if (t !== "dark" && t !== "light") t = "dark";
|
|
25
|
+
document.documentElement.dataset.theme = t;
|
|
26
|
+
} catch {}
|
|
27
|
+
</script>
|
|
19
28
|
</head>
|
|
20
29
|
<body>
|
|
21
30
|
<div id="app"></div>
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
const childProcess = require("child_process");
|
|
2
1
|
const fs = require("fs");
|
|
3
|
-
const os = require("os");
|
|
4
2
|
const path = require("path");
|
|
5
3
|
const https = require("https");
|
|
6
4
|
const http = require("http");
|
|
@@ -8,7 +6,6 @@ const {
|
|
|
8
6
|
kLatestVersionCacheTtlMs,
|
|
9
7
|
kAlphaclawRegistryUrl,
|
|
10
8
|
kNpmPackageRoot,
|
|
11
|
-
kOpenclawUpdateCopyTimeoutMs,
|
|
12
9
|
kRootDir,
|
|
13
10
|
} = require("./constants");
|
|
14
11
|
|
|
@@ -26,6 +23,9 @@ const isNewerVersion = (latest, current) => {
|
|
|
26
23
|
return l.patch > c.patch;
|
|
27
24
|
};
|
|
28
25
|
|
|
26
|
+
const buildAlphaclawInstallSpec = (version = "latest") =>
|
|
27
|
+
`@chrysb/alphaclaw@${String(version || "").trim() || "latest"}`;
|
|
28
|
+
|
|
29
29
|
const createAlphaclawVersionService = () => {
|
|
30
30
|
let kUpdateStatusCache = {
|
|
31
31
|
latestVersion: null,
|
|
@@ -108,120 +108,6 @@ const createAlphaclawVersionService = () => {
|
|
|
108
108
|
return { latestVersion, hasUpdate };
|
|
109
109
|
};
|
|
110
110
|
|
|
111
|
-
const findInstallDir = () => {
|
|
112
|
-
// Walk up from kNpmPackageRoot to find the consuming project's directory
|
|
113
|
-
// (the one with node_modules/@chrysb/alphaclaw). In Docker this is /app.
|
|
114
|
-
let dir = kNpmPackageRoot;
|
|
115
|
-
while (dir !== path.dirname(dir)) {
|
|
116
|
-
const parent = path.dirname(dir);
|
|
117
|
-
if (
|
|
118
|
-
path.basename(parent) === "node_modules" ||
|
|
119
|
-
parent.includes(`${path.sep}node_modules${path.sep}`)
|
|
120
|
-
) {
|
|
121
|
-
dir = parent;
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
const pkgPath = path.join(parent, "package.json");
|
|
125
|
-
if (fs.existsSync(pkgPath)) {
|
|
126
|
-
try {
|
|
127
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
128
|
-
if (
|
|
129
|
-
pkg.dependencies?.["@chrysb/alphaclaw"] ||
|
|
130
|
-
pkg.devDependencies?.["@chrysb/alphaclaw"] ||
|
|
131
|
-
pkg.optionalDependencies?.["@chrysb/alphaclaw"]
|
|
132
|
-
) {
|
|
133
|
-
return parent;
|
|
134
|
-
}
|
|
135
|
-
} catch {}
|
|
136
|
-
}
|
|
137
|
-
dir = parent;
|
|
138
|
-
}
|
|
139
|
-
// Fallback: if running directly (not from node_modules), use kNpmPackageRoot
|
|
140
|
-
return kNpmPackageRoot;
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
const installLatestAlphaclaw = () =>
|
|
144
|
-
new Promise((resolve, reject) => {
|
|
145
|
-
const installDir = findInstallDir();
|
|
146
|
-
const tmpDir = fs.mkdtempSync(
|
|
147
|
-
path.join(os.tmpdir(), "alphaclaw-update-"),
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
const cleanup = () => {
|
|
151
|
-
try {
|
|
152
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
153
|
-
} catch {}
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
fs.writeFileSync(
|
|
157
|
-
path.join(tmpDir, "package.json"),
|
|
158
|
-
JSON.stringify({
|
|
159
|
-
private: true,
|
|
160
|
-
dependencies: { "@chrysb/alphaclaw": "latest" },
|
|
161
|
-
}),
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
const npmEnv = {
|
|
165
|
-
...process.env,
|
|
166
|
-
npm_config_update_notifier: "false",
|
|
167
|
-
npm_config_fund: "false",
|
|
168
|
-
npm_config_audit: "false",
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
console.log(
|
|
172
|
-
`[alphaclaw] Running: npm install @chrysb/alphaclaw@latest in temp dir (target: ${installDir})`,
|
|
173
|
-
);
|
|
174
|
-
childProcess.exec(
|
|
175
|
-
"npm install --omit=dev --prefer-online --package-lock=false",
|
|
176
|
-
{
|
|
177
|
-
cwd: tmpDir,
|
|
178
|
-
env: npmEnv,
|
|
179
|
-
timeout: 180000,
|
|
180
|
-
},
|
|
181
|
-
(err, stdout, stderr) => {
|
|
182
|
-
if (err) {
|
|
183
|
-
const message = String(stderr || err.message || "").trim();
|
|
184
|
-
console.log(
|
|
185
|
-
`[alphaclaw] alphaclaw install error: ${message.slice(0, 200)}`,
|
|
186
|
-
);
|
|
187
|
-
cleanup();
|
|
188
|
-
return reject(
|
|
189
|
-
new Error(
|
|
190
|
-
message || "Failed to install @chrysb/alphaclaw@latest",
|
|
191
|
-
),
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
if (stdout?.trim()) {
|
|
195
|
-
console.log(
|
|
196
|
-
`[alphaclaw] alphaclaw install stdout: ${stdout.trim().slice(0, 300)}`,
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const src = path.join(tmpDir, "node_modules");
|
|
201
|
-
const dest = path.join(installDir, "node_modules");
|
|
202
|
-
childProcess.exec(
|
|
203
|
-
`cp -af "${src}/." "${dest}/"`,
|
|
204
|
-
{ timeout: kOpenclawUpdateCopyTimeoutMs },
|
|
205
|
-
(copyErr) => {
|
|
206
|
-
cleanup();
|
|
207
|
-
if (copyErr) {
|
|
208
|
-
console.log(
|
|
209
|
-
`[alphaclaw] alphaclaw copy error: ${(copyErr.message || "").slice(0, 200)}`,
|
|
210
|
-
);
|
|
211
|
-
return reject(
|
|
212
|
-
new Error(
|
|
213
|
-
`Failed to copy updated AlphaClaw files: ${copyErr.message}`,
|
|
214
|
-
),
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
console.log("[alphaclaw] alphaclaw install completed");
|
|
218
|
-
resolve({ stdout: stdout?.trim(), stderr: stderr?.trim() });
|
|
219
|
-
},
|
|
220
|
-
);
|
|
221
|
-
},
|
|
222
|
-
);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
111
|
const isContainer = () =>
|
|
226
112
|
process.env.RAILWAY_ENVIRONMENT ||
|
|
227
113
|
process.env.RENDER ||
|
|
@@ -277,18 +163,33 @@ const createAlphaclawVersionService = () => {
|
|
|
277
163
|
kUpdateInProgress = true;
|
|
278
164
|
const previousVersion = readAlphaclawVersion();
|
|
279
165
|
try {
|
|
280
|
-
|
|
281
|
-
// Write marker to persistent volume so the update survives container recreation
|
|
282
|
-
const markerPath = path.join(kRootDir, ".alphaclaw-update-pending");
|
|
166
|
+
let targetVersion = "latest";
|
|
283
167
|
try {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
168
|
+
const updateStatus = await readAlphaclawUpdateStatus({ refresh: true });
|
|
169
|
+
if (updateStatus.latestVersion) {
|
|
170
|
+
targetVersion = updateStatus.latestVersion;
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.log(
|
|
174
|
+
`[alphaclaw] Could not resolve exact AlphaClaw version before restart: ${error.message || "unknown error"}`,
|
|
287
175
|
);
|
|
288
|
-
console.log(`[alphaclaw] Update marker written to ${markerPath}`);
|
|
289
|
-
} catch (e) {
|
|
290
|
-
console.log(`[alphaclaw] Could not write update marker: ${e.message}`);
|
|
291
176
|
}
|
|
177
|
+
|
|
178
|
+
const spec = buildAlphaclawInstallSpec(targetVersion);
|
|
179
|
+
// Write marker to persistent volume so the update survives container recreation
|
|
180
|
+
const markerPath = path.join(kRootDir, ".alphaclaw-update-pending");
|
|
181
|
+
fs.writeFileSync(
|
|
182
|
+
markerPath,
|
|
183
|
+
JSON.stringify({
|
|
184
|
+
from: previousVersion,
|
|
185
|
+
to: targetVersion,
|
|
186
|
+
spec,
|
|
187
|
+
ts: Date.now(),
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
console.log(
|
|
191
|
+
`[alphaclaw] Update marker written to ${markerPath} for ${spec}`,
|
|
192
|
+
);
|
|
292
193
|
kUpdateStatusCache = {
|
|
293
194
|
latestVersion: null,
|
|
294
195
|
hasUpdate: false,
|
|
@@ -299,15 +200,17 @@ const createAlphaclawVersionService = () => {
|
|
|
299
200
|
body: {
|
|
300
201
|
ok: true,
|
|
301
202
|
previousVersion,
|
|
203
|
+
targetVersion: targetVersion === "latest" ? null : targetVersion,
|
|
302
204
|
restarting: true,
|
|
303
205
|
},
|
|
304
206
|
};
|
|
305
207
|
} catch (err) {
|
|
306
|
-
kUpdateInProgress = false;
|
|
307
208
|
return {
|
|
308
209
|
status: 500,
|
|
309
210
|
body: { ok: false, error: err.message || "Failed to update AlphaClaw" },
|
|
310
211
|
};
|
|
212
|
+
} finally {
|
|
213
|
+
kUpdateInProgress = false;
|
|
311
214
|
}
|
|
312
215
|
};
|
|
313
216
|
|
|
@@ -10,6 +10,7 @@ let pruneTimer = null;
|
|
|
10
10
|
const kDefaultRequestLimit = 50;
|
|
11
11
|
const kMaxRequestLimit = 200;
|
|
12
12
|
const kPruneIntervalMs = 12 * 60 * 60 * 1000;
|
|
13
|
+
const kHealthSummaryWindow = 25;
|
|
13
14
|
|
|
14
15
|
const ensureDb = () => {
|
|
15
16
|
if (!db) throw new Error("Webhooks DB not initialized");
|
|
@@ -202,22 +203,61 @@ const getHookSummaries = () => {
|
|
|
202
203
|
const database = ensureDb();
|
|
203
204
|
const rows = database
|
|
204
205
|
.prepare(`
|
|
206
|
+
WITH ranked_requests AS (
|
|
207
|
+
SELECT
|
|
208
|
+
hook_name,
|
|
209
|
+
created_at,
|
|
210
|
+
gateway_status,
|
|
211
|
+
ROW_NUMBER() OVER (
|
|
212
|
+
PARTITION BY hook_name
|
|
213
|
+
ORDER BY created_at DESC, id DESC
|
|
214
|
+
) AS row_num
|
|
215
|
+
FROM webhook_requests
|
|
216
|
+
),
|
|
217
|
+
overall_counts AS (
|
|
218
|
+
SELECT
|
|
219
|
+
hook_name,
|
|
220
|
+
MAX(created_at) AS last_received,
|
|
221
|
+
COUNT(*) AS total_count,
|
|
222
|
+
SUM(CASE WHEN gateway_status >= 200 AND gateway_status < 300 THEN 1 ELSE 0 END) AS success_count,
|
|
223
|
+
SUM(CASE WHEN gateway_status IS NULL OR gateway_status < 200 OR gateway_status >= 300 THEN 1 ELSE 0 END) AS error_count
|
|
224
|
+
FROM webhook_requests
|
|
225
|
+
GROUP BY hook_name
|
|
226
|
+
),
|
|
227
|
+
recent_counts AS (
|
|
228
|
+
SELECT
|
|
229
|
+
hook_name,
|
|
230
|
+
COUNT(*) AS recent_total_count,
|
|
231
|
+
SUM(CASE WHEN gateway_status >= 200 AND gateway_status < 300 THEN 1 ELSE 0 END) AS recent_success_count,
|
|
232
|
+
SUM(CASE WHEN gateway_status IS NULL OR gateway_status < 200 OR gateway_status >= 300 THEN 1 ELSE 0 END) AS recent_error_count
|
|
233
|
+
FROM ranked_requests
|
|
234
|
+
WHERE row_num <= $health_window
|
|
235
|
+
GROUP BY hook_name
|
|
236
|
+
)
|
|
205
237
|
SELECT
|
|
206
|
-
hook_name,
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
238
|
+
overall_counts.hook_name,
|
|
239
|
+
overall_counts.last_received,
|
|
240
|
+
overall_counts.total_count,
|
|
241
|
+
overall_counts.success_count,
|
|
242
|
+
overall_counts.error_count,
|
|
243
|
+
COALESCE(recent_counts.recent_total_count, 0) AS recent_total_count,
|
|
244
|
+
COALESCE(recent_counts.recent_success_count, 0) AS recent_success_count,
|
|
245
|
+
COALESCE(recent_counts.recent_error_count, 0) AS recent_error_count
|
|
246
|
+
FROM overall_counts
|
|
247
|
+
LEFT JOIN recent_counts
|
|
248
|
+
ON recent_counts.hook_name = overall_counts.hook_name
|
|
213
249
|
`)
|
|
214
|
-
.all();
|
|
250
|
+
.all({ $health_window: kHealthSummaryWindow });
|
|
215
251
|
return rows.map((row) => ({
|
|
216
252
|
hookName: row.hook_name,
|
|
217
253
|
lastReceived: row.last_received || null,
|
|
218
254
|
totalCount: Number(row.total_count || 0),
|
|
219
255
|
successCount: Number(row.success_count || 0),
|
|
220
256
|
errorCount: Number(row.error_count || 0),
|
|
257
|
+
recentTotalCount: Number(row.recent_total_count || 0),
|
|
258
|
+
recentSuccessCount: Number(row.recent_success_count || 0),
|
|
259
|
+
recentErrorCount: Number(row.recent_error_count || 0),
|
|
260
|
+
healthWindowSize: kHealthSummaryWindow,
|
|
221
261
|
}));
|
|
222
262
|
};
|
|
223
263
|
|
|
@@ -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
|
+
};
|