@chrysb/alphaclaw 0.9.0-beta.6 → 0.9.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 +25 -25
- package/lib/cli/git-runtime.js +97 -0
- package/lib/public/css/chat.css +0 -12
- 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 +2770 -2762
- package/lib/public/js/app.js +26 -14
- package/lib/public/js/components/agents-tab/create-channel-modal.js +259 -59
- package/lib/public/js/components/gateway.js +0 -286
- package/lib/public/js/components/general/index.js +0 -7
- package/lib/public/js/components/icons.js +26 -25
- package/lib/public/js/components/modal-shell.js +1 -1
- 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/routes/chat-route.js +2 -9
- package/lib/public/js/components/routes/general-route.js +0 -6
- package/lib/public/js/components/routes/index.js +0 -1
- package/lib/public/js/components/routes/watchdog-route.js +0 -6
- package/lib/public/js/components/sidebar.js +21 -7
- package/lib/public/js/components/theme-toggle.js +113 -0
- package/lib/public/js/components/update-modal.js +174 -51
- package/lib/public/js/components/watchdog-tab/index.js +0 -6
- 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/hooks/use-app-shell-controller.js +16 -33
- package/lib/public/js/lib/api.js +0 -28
- package/lib/public/js/lib/app-navigation.js +0 -2
- package/lib/public/js/lib/channel-provider-availability.js +1 -2
- 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/scripts/git +47 -1
- package/lib/server/agents/channels.js +1 -4
- package/lib/server/alphaclaw-version.js +590 -132
- package/lib/server/constants.js +5 -0
- package/lib/server/db/webhooks/index.js +48 -8
- package/lib/server/exec-defaults-config.js +163 -0
- package/lib/server/init/register-server-routes.js +0 -8
- package/lib/server/init/server-lifecycle.js +2 -0
- package/lib/server/model-catalog-cache.js +251 -0
- package/lib/server/onboarding/index.js +5 -0
- package/lib/server/routes/models.js +14 -23
- package/lib/server/routes/nodes.js +9 -23
- package/lib/server/routes/system.js +3 -16
- package/lib/server/routes/webhooks.js +12 -1
- package/lib/server/startup.js +8 -0
- package/lib/server/watchdog-notify.js +172 -55
- package/lib/server.js +17 -2
- package/package.json +2 -2
- package/patches/openclaw+2026.4.9.patch +13 -0
- package/lib/public/js/components/mcp-tab/index.js +0 -237
- package/lib/public/js/components/routes/mcp-route.js +0 -7
- package/lib/server/mcp-bridge.js +0 -158
- package/lib/server/routes/mcp.js +0 -252
- package/patches/openclaw+2026.3.28.patch +0 -13
package/lib/server/constants.js
CHANGED
|
@@ -176,6 +176,11 @@ const kSystemVars = new Set([
|
|
|
176
176
|
"OPENCLAW_GATEWAY_TOKEN",
|
|
177
177
|
"SETUP_PASSWORD",
|
|
178
178
|
"PORT",
|
|
179
|
+
"ALPHACLAW_DEPLOYMENT_PROVIDER",
|
|
180
|
+
"ALPHACLAW_MANAGED_UPDATE_URL",
|
|
181
|
+
"ALPHACLAW_MANAGED_UPDATE_TOKEN",
|
|
182
|
+
"ALPHACLAW_TEMPLATE_REPO_URL",
|
|
183
|
+
"ALPHACLAW_TEMPLATE_BRANCH",
|
|
179
184
|
"WATCHDOG_AUTO_REPAIR",
|
|
180
185
|
"WATCHDOG_NOTIFICATIONS_DISABLED",
|
|
181
186
|
]);
|
|
@@ -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,163 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const {
|
|
4
|
+
readOpenclawConfig,
|
|
5
|
+
resolveOpenclawConfigPath,
|
|
6
|
+
writeOpenclawConfig,
|
|
7
|
+
} = require("./openclaw-config");
|
|
8
|
+
|
|
9
|
+
const kManagedExecApprovalsDefaults = Object.freeze({
|
|
10
|
+
security: "full",
|
|
11
|
+
ask: "off",
|
|
12
|
+
askFallback: "full",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const kManagedOpenclawExecDefaults = Object.freeze({
|
|
16
|
+
security: "full",
|
|
17
|
+
strictInlineEval: false,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const resolveExecApprovalsConfigPath = ({ openclawDir }) =>
|
|
21
|
+
path.join(openclawDir, "exec-approvals.json");
|
|
22
|
+
|
|
23
|
+
const readExecApprovalsConfig = ({
|
|
24
|
+
fsModule = fs,
|
|
25
|
+
openclawDir,
|
|
26
|
+
fallback = { version: 1 },
|
|
27
|
+
} = {}) => {
|
|
28
|
+
const filePath = resolveExecApprovalsConfigPath({ openclawDir });
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(fsModule.readFileSync(filePath, "utf8"));
|
|
31
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
32
|
+
? parsed
|
|
33
|
+
: fallback;
|
|
34
|
+
} catch {
|
|
35
|
+
return fallback;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const writeExecApprovalsConfig = ({
|
|
40
|
+
fsModule = fs,
|
|
41
|
+
openclawDir,
|
|
42
|
+
file = {},
|
|
43
|
+
spacing = 2,
|
|
44
|
+
} = {}) => {
|
|
45
|
+
const filePath = resolveExecApprovalsConfigPath({ openclawDir });
|
|
46
|
+
fsModule.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
47
|
+
fsModule.writeFileSync(filePath, JSON.stringify(file, null, spacing) + "\n", "utf8");
|
|
48
|
+
return filePath;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const hasOwn = (obj, key) =>
|
|
52
|
+
!!obj && typeof obj === "object" && Object.prototype.hasOwnProperty.call(obj, key);
|
|
53
|
+
|
|
54
|
+
const ensureManagedExecApprovalsDefaults = (rawFile = {}) => {
|
|
55
|
+
const file =
|
|
56
|
+
rawFile && typeof rawFile === "object" && !Array.isArray(rawFile) ? rawFile : {};
|
|
57
|
+
const before = JSON.stringify(file);
|
|
58
|
+
const defaults =
|
|
59
|
+
file.defaults && typeof file.defaults === "object" && !Array.isArray(file.defaults)
|
|
60
|
+
? file.defaults
|
|
61
|
+
: null;
|
|
62
|
+
const hasNonEmptyDefaults = !!defaults && Object.keys(defaults).length > 0;
|
|
63
|
+
if (!hasNonEmptyDefaults) {
|
|
64
|
+
if (!Number.isInteger(file.version)) file.version = 1;
|
|
65
|
+
file.defaults = {
|
|
66
|
+
security: kManagedExecApprovalsDefaults.security,
|
|
67
|
+
ask: kManagedExecApprovalsDefaults.ask,
|
|
68
|
+
askFallback: kManagedExecApprovalsDefaults.askFallback,
|
|
69
|
+
};
|
|
70
|
+
if (!file.agents || typeof file.agents !== "object" || Array.isArray(file.agents)) {
|
|
71
|
+
file.agents = {};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
file,
|
|
76
|
+
changed: JSON.stringify(file) !== before,
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const ensureManagedOpenclawExecDefaults = (rawConfig = {}) => {
|
|
81
|
+
const config =
|
|
82
|
+
rawConfig && typeof rawConfig === "object" && !Array.isArray(rawConfig) ? rawConfig : {};
|
|
83
|
+
const before = JSON.stringify(config);
|
|
84
|
+
if (!config.tools || typeof config.tools !== "object" || Array.isArray(config.tools)) {
|
|
85
|
+
config.tools = {};
|
|
86
|
+
}
|
|
87
|
+
if (!hasOwn(config.tools, "exec")) {
|
|
88
|
+
config.tools.exec = {
|
|
89
|
+
security: kManagedOpenclawExecDefaults.security,
|
|
90
|
+
strictInlineEval: kManagedOpenclawExecDefaults.strictInlineEval,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
config,
|
|
95
|
+
changed: JSON.stringify(config) !== before,
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const ensureManagedExecDefaults = ({ fsModule = fs, openclawDir } = {}) => {
|
|
100
|
+
let openclawChanged = false;
|
|
101
|
+
let approvalsChanged = false;
|
|
102
|
+
|
|
103
|
+
const openclawConfigPath = resolveOpenclawConfigPath({ openclawDir });
|
|
104
|
+
const openclawExists =
|
|
105
|
+
typeof fsModule.existsSync === "function" ? fsModule.existsSync(openclawConfigPath) : null;
|
|
106
|
+
if (openclawExists !== false) {
|
|
107
|
+
const cfg = readOpenclawConfig({
|
|
108
|
+
fsModule,
|
|
109
|
+
openclawDir,
|
|
110
|
+
fallback: openclawExists === true ? null : {},
|
|
111
|
+
});
|
|
112
|
+
if (cfg && typeof cfg === "object" && !Array.isArray(cfg)) {
|
|
113
|
+
const ensuredConfig = ensureManagedOpenclawExecDefaults(cfg);
|
|
114
|
+
if (ensuredConfig.changed) {
|
|
115
|
+
writeOpenclawConfig({
|
|
116
|
+
fsModule,
|
|
117
|
+
openclawDir,
|
|
118
|
+
config: ensuredConfig.config,
|
|
119
|
+
spacing: 2,
|
|
120
|
+
});
|
|
121
|
+
openclawChanged = true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const approvalsPath = resolveExecApprovalsConfigPath({ openclawDir });
|
|
127
|
+
const approvalsExists =
|
|
128
|
+
typeof fsModule.existsSync === "function" ? fsModule.existsSync(approvalsPath) : null;
|
|
129
|
+
const approvals = readExecApprovalsConfig({
|
|
130
|
+
fsModule,
|
|
131
|
+
openclawDir,
|
|
132
|
+
fallback: approvalsExists === true ? null : { version: 1 },
|
|
133
|
+
});
|
|
134
|
+
if (approvals && typeof approvals === "object" && !Array.isArray(approvals)) {
|
|
135
|
+
const ensuredApprovals = ensureManagedExecApprovalsDefaults(approvals);
|
|
136
|
+
if (ensuredApprovals.changed || approvalsExists === false) {
|
|
137
|
+
writeExecApprovalsConfig({
|
|
138
|
+
fsModule,
|
|
139
|
+
openclawDir,
|
|
140
|
+
file: ensuredApprovals.file,
|
|
141
|
+
spacing: 2,
|
|
142
|
+
});
|
|
143
|
+
approvalsChanged = true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
changed: openclawChanged || approvalsChanged,
|
|
149
|
+
openclawChanged,
|
|
150
|
+
approvalsChanged,
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
module.exports = {
|
|
155
|
+
kManagedExecApprovalsDefaults,
|
|
156
|
+
kManagedOpenclawExecDefaults,
|
|
157
|
+
resolveExecApprovalsConfigPath,
|
|
158
|
+
readExecApprovalsConfig,
|
|
159
|
+
writeExecApprovalsConfig,
|
|
160
|
+
ensureManagedExecApprovalsDefaults,
|
|
161
|
+
ensureManagedOpenclawExecDefaults,
|
|
162
|
+
ensureManagedExecDefaults,
|
|
163
|
+
};
|
|
@@ -17,7 +17,6 @@ const { registerDoctorRoutes } = require("../routes/doctor");
|
|
|
17
17
|
const { registerAgentRoutes } = require("../routes/agents");
|
|
18
18
|
const { registerCronRoutes } = require("../routes/cron");
|
|
19
19
|
const { registerNodeRoutes } = require("../routes/nodes");
|
|
20
|
-
const { registerMcpRoutes } = require("../routes/mcp");
|
|
21
20
|
const {
|
|
22
21
|
createOauthCallbackMiddleware,
|
|
23
22
|
} = require("../oauth-callback-middleware");
|
|
@@ -250,13 +249,6 @@ const registerServerRoutes = ({
|
|
|
250
249
|
gatewayToken: constants.GATEWAY_TOKEN,
|
|
251
250
|
fsModule: fs,
|
|
252
251
|
});
|
|
253
|
-
registerMcpRoutes({
|
|
254
|
-
app,
|
|
255
|
-
requireAuth,
|
|
256
|
-
constants,
|
|
257
|
-
gatewayEnv,
|
|
258
|
-
openclawDir: constants.OPENCLAW_DIR,
|
|
259
|
-
});
|
|
260
252
|
registerProxyRoutes({
|
|
261
253
|
app,
|
|
262
254
|
proxy,
|
|
@@ -3,6 +3,7 @@ const startServerLifecycle = ({
|
|
|
3
3
|
PORT,
|
|
4
4
|
isOnboarded,
|
|
5
5
|
runOnboardedBootSequence,
|
|
6
|
+
ensureManagedExecDefaults,
|
|
6
7
|
ensureUsageTrackerPluginConfig,
|
|
7
8
|
doSyncPromptFiles,
|
|
8
9
|
reloadEnv,
|
|
@@ -18,6 +19,7 @@ const startServerLifecycle = ({
|
|
|
18
19
|
console.log(`[alphaclaw] Express listening on :${PORT}`);
|
|
19
20
|
if (isOnboarded()) {
|
|
20
21
|
runOnboardedBootSequence({
|
|
22
|
+
ensureManagedExecDefaults,
|
|
21
23
|
ensureUsageTrackerPluginConfig,
|
|
22
24
|
doSyncPromptFiles,
|
|
23
25
|
reloadEnv,
|
|
@@ -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
|
+
};
|
|
@@ -25,6 +25,7 @@ const {
|
|
|
25
25
|
} = require("./cron");
|
|
26
26
|
const { migrateManagedInternalFiles } = require("../internal-files-migration");
|
|
27
27
|
const { installGogCliSkill } = require("../gog-skill");
|
|
28
|
+
const { ensureManagedExecDefaults } = require("../exec-defaults-config");
|
|
28
29
|
|
|
29
30
|
const kPlaceholderEnvValue = "placeholder";
|
|
30
31
|
const kEnvRefPattern = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
|
|
@@ -529,6 +530,10 @@ const createOnboardingService = ({
|
|
|
529
530
|
});
|
|
530
531
|
}
|
|
531
532
|
authProfiles?.syncConfigAuthReferencesForAgent?.();
|
|
533
|
+
ensureManagedExecDefaults({
|
|
534
|
+
fsModule: fs,
|
|
535
|
+
openclawDir: OPENCLAW_DIR,
|
|
536
|
+
});
|
|
532
537
|
|
|
533
538
|
installGogCliSkill({ fs, openclawDir: OPENCLAW_DIR });
|
|
534
539
|
|
|
@@ -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
|