@chrysb/alphaclaw 0.9.0-beta.7 → 0.9.1-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/alphaclaw.js +26 -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 +107 -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 +31 -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 +110 -16
- 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/gateway.js +1 -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/github.js +83 -2
- package/lib/server/onboarding/index.js +7 -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/lib/setup/core-prompts/AGENTS.md +12 -0
- package/lib/setup/core-prompts/TOOLS.md +12 -0
- 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 -292
- 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
|
+
};
|
package/lib/server/gateway.js
CHANGED
|
@@ -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: 30000,
|
|
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
|
+
};
|
|
@@ -63,6 +63,56 @@ const repoContainsOnlyBoilerplate = async (repoUrl, ghHeaders) => {
|
|
|
63
63
|
}
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
+
const getNextGithubPageUrl = (linkHeader = "") => {
|
|
67
|
+
const nextLink = String(linkHeader || "")
|
|
68
|
+
.split(",")
|
|
69
|
+
.map((entry) => entry.trim())
|
|
70
|
+
.find((entry) => entry.endsWith('rel="next"'));
|
|
71
|
+
const match = nextLink?.match(/<([^>]+)>/);
|
|
72
|
+
return match?.[1] || "";
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const findOwnedRepoByName = async ({
|
|
76
|
+
repoUrl,
|
|
77
|
+
repoOwner,
|
|
78
|
+
repoName,
|
|
79
|
+
viewerLogin,
|
|
80
|
+
ghHeaders,
|
|
81
|
+
}) => {
|
|
82
|
+
if (
|
|
83
|
+
!repoOwner ||
|
|
84
|
+
!repoName ||
|
|
85
|
+
!viewerLogin ||
|
|
86
|
+
repoOwner.toLowerCase() !== viewerLogin.toLowerCase()
|
|
87
|
+
) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let nextUrl =
|
|
92
|
+
"https://api.github.com/user/repos?affiliation=owner&per_page=100&page=1";
|
|
93
|
+
const normalizedRepoUrl = String(repoUrl || "").trim().toLowerCase();
|
|
94
|
+
const normalizedRepoName = String(repoName || "").trim().toLowerCase();
|
|
95
|
+
|
|
96
|
+
while (nextUrl) {
|
|
97
|
+
const res = await fetch(nextUrl, { headers: ghHeaders });
|
|
98
|
+
if (!res.ok) return null;
|
|
99
|
+
|
|
100
|
+
const repos = await res.json();
|
|
101
|
+
if (!Array.isArray(repos)) return null;
|
|
102
|
+
|
|
103
|
+
const existingRepo = repos.find((repo) => {
|
|
104
|
+
const fullName = String(repo?.full_name || "").trim().toLowerCase();
|
|
105
|
+
const name = String(repo?.name || "").trim().toLowerCase();
|
|
106
|
+
return fullName === normalizedRepoUrl || name === normalizedRepoName;
|
|
107
|
+
});
|
|
108
|
+
if (existingRepo) return existingRepo;
|
|
109
|
+
|
|
110
|
+
nextUrl = getNextGithubPageUrl(res.headers?.get?.("link"));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return null;
|
|
114
|
+
};
|
|
115
|
+
|
|
66
116
|
const isClassicPat = (token) => String(token || "").startsWith("ghp_");
|
|
67
117
|
const isFineGrainedPat = (token) =>
|
|
68
118
|
String(token || "").startsWith("github_pat_");
|
|
@@ -73,8 +123,9 @@ const verifyGithubRepoForOnboarding = async ({
|
|
|
73
123
|
mode = "new",
|
|
74
124
|
}) => {
|
|
75
125
|
const ghHeaders = buildGithubHeaders(githubToken);
|
|
76
|
-
const [repoOwner] = String(repoUrl || "").split("/"
|
|
126
|
+
const [repoOwner = "", repoName = ""] = String(repoUrl || "").split("/");
|
|
77
127
|
const isExisting = mode === "existing";
|
|
128
|
+
let viewerLogin = "";
|
|
78
129
|
|
|
79
130
|
try {
|
|
80
131
|
const userRes = await fetch("https://api.github.com/user", {
|
|
@@ -106,12 +157,29 @@ const verifyGithubRepoForOnboarding = async ({
|
|
|
106
157
|
};
|
|
107
158
|
}
|
|
108
159
|
}
|
|
109
|
-
await userRes.json().catch(() => ({}));
|
|
160
|
+
const userPayload = await userRes.json().catch(() => ({}));
|
|
161
|
+
viewerLogin = String(userPayload?.login || "").trim();
|
|
110
162
|
|
|
111
163
|
const checkRes = await fetch(`https://api.github.com/repos/${repoUrl}`, {
|
|
112
164
|
headers: ghHeaders,
|
|
113
165
|
});
|
|
114
166
|
if (checkRes.status === 404) {
|
|
167
|
+
const hiddenOwnedRepo = await findOwnedRepoByName({
|
|
168
|
+
repoUrl,
|
|
169
|
+
repoOwner,
|
|
170
|
+
repoName,
|
|
171
|
+
viewerLogin,
|
|
172
|
+
ghHeaders,
|
|
173
|
+
});
|
|
174
|
+
if (hiddenOwnedRepo) {
|
|
175
|
+
return {
|
|
176
|
+
ok: false,
|
|
177
|
+
status: 400,
|
|
178
|
+
error:
|
|
179
|
+
`Repository "${repoUrl}" already exists, but this token cannot inspect it. ` +
|
|
180
|
+
"Choose a different repo name or use a token that can access that repo.",
|
|
181
|
+
};
|
|
182
|
+
}
|
|
115
183
|
if (isExisting) {
|
|
116
184
|
return {
|
|
117
185
|
ok: false,
|
|
@@ -205,6 +273,19 @@ const ensureGithubRepoAccessible = async ({
|
|
|
205
273
|
});
|
|
206
274
|
if (!createRes.ok) {
|
|
207
275
|
const details = await parseGithubErrorMessage(createRes);
|
|
276
|
+
if (
|
|
277
|
+
String(details || "")
|
|
278
|
+
.toLowerCase()
|
|
279
|
+
.includes("name already exists on this account")
|
|
280
|
+
) {
|
|
281
|
+
return {
|
|
282
|
+
ok: false,
|
|
283
|
+
status: 400,
|
|
284
|
+
error:
|
|
285
|
+
`Repository "${repoUrl}" already exists. ` +
|
|
286
|
+
"Choose a different repo name or use a token that can access that repo.",
|
|
287
|
+
};
|
|
288
|
+
}
|
|
208
289
|
const hint =
|
|
209
290
|
createRes.status === 404 || createRes.status === 403
|
|
210
291
|
? ' Ensure your token is a classic PAT with the "repo" scope.'
|
|
@@ -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;
|
|
@@ -494,6 +495,8 @@ const createOnboardingService = ({
|
|
|
494
495
|
...process.env,
|
|
495
496
|
OPENCLAW_HOME: kRootDir,
|
|
496
497
|
OPENCLAW_CONFIG_PATH: `${OPENCLAW_DIR}/openclaw.json`,
|
|
498
|
+
OPENCLAW_STATE_DIR: OPENCLAW_DIR,
|
|
499
|
+
XDG_CONFIG_HOME: OPENCLAW_DIR,
|
|
497
500
|
},
|
|
498
501
|
timeout: 120000,
|
|
499
502
|
},
|
|
@@ -529,6 +532,10 @@ const createOnboardingService = ({
|
|
|
529
532
|
});
|
|
530
533
|
}
|
|
531
534
|
authProfiles?.syncConfigAuthReferencesForAgent?.();
|
|
535
|
+
ensureManagedExecDefaults({
|
|
536
|
+
fsModule: fs,
|
|
537
|
+
openclawDir: OPENCLAW_DIR,
|
|
538
|
+
});
|
|
532
539
|
|
|
533
540
|
installGogCliSkill({ fs, openclawDir: OPENCLAW_DIR });
|
|
534
541
|
|