@chrysb/alphaclaw 0.9.17 → 0.9.18
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/README.md +25 -0
- package/lib/public/dist/app.bundle.js +1265 -1225
- package/lib/public/js/components/api-feature-panel.js +76 -0
- package/lib/public/js/components/general/index.js +6 -0
- package/lib/public/js/components/general/use-general-tab.js +69 -0
- package/lib/public/js/lib/api.js +19 -0
- package/lib/public/js/lib/storage-keys.js +4 -0
- package/lib/server/alphaclaw-config.js +99 -0
- package/lib/server/constants.js +48 -0
- package/lib/server/gateway.js +163 -1
- package/lib/server/init/register-server-routes.js +8 -0
- package/lib/server/login-throttle.js +41 -22
- package/lib/server/onboarding/openclaw.js +27 -3
- package/lib/server/routes/proxy.js +219 -1
- package/lib/server/routes/system.js +61 -0
- package/lib/server.js +35 -1
- package/package.json +1 -1
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
} = require("./constants");
|
|
12
12
|
|
|
13
13
|
const kGlobalStateKey = "global:login";
|
|
14
|
+
const kLoginThrottleScope = "login";
|
|
14
15
|
|
|
15
16
|
const createLoginAttemptState = (now) => ({
|
|
16
17
|
attempts: 0,
|
|
@@ -48,8 +49,15 @@ const createMemoryLoginThrottleStore = () => {
|
|
|
48
49
|
};
|
|
49
50
|
};
|
|
50
51
|
|
|
51
|
-
const getClientStateKey = (clientKey) =>
|
|
52
|
-
|
|
52
|
+
const getClientStateKey = (clientKey, scope = kLoginThrottleScope) => {
|
|
53
|
+
const normalizedClientKey = String(clientKey || "unknown");
|
|
54
|
+
return scope === kLoginThrottleScope
|
|
55
|
+
? `client:${normalizedClientKey}`
|
|
56
|
+
: `client:${scope}:${normalizedClientKey}`;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const getGlobalStateKey = (scope = kLoginThrottleScope) =>
|
|
60
|
+
scope === kLoginThrottleScope ? kGlobalStateKey : `global:${scope}`;
|
|
53
61
|
|
|
54
62
|
const getOrCreateState = (store, stateKey, now) => {
|
|
55
63
|
const existing = store.get(stateKey);
|
|
@@ -153,6 +161,16 @@ const chooseFailureResult = (...results) => {
|
|
|
153
161
|
|
|
154
162
|
const createLoginThrottle = ({
|
|
155
163
|
store = createMemoryLoginThrottleStore(),
|
|
164
|
+
scope = kLoginThrottleScope,
|
|
165
|
+
windowMs = kLoginWindowMs,
|
|
166
|
+
maxAttempts = kLoginMaxAttempts,
|
|
167
|
+
baseLockMs = kLoginBaseLockMs,
|
|
168
|
+
maxLockMs = kLoginMaxLockMs,
|
|
169
|
+
globalWindowMs = kLoginGlobalWindowMs,
|
|
170
|
+
globalMaxAttempts = kLoginGlobalMaxAttempts,
|
|
171
|
+
globalBaseLockMs = kLoginGlobalBaseLockMs,
|
|
172
|
+
globalMaxLockMs = kLoginGlobalMaxLockMs,
|
|
173
|
+
stateTtlMs = kLoginStateTtlMs,
|
|
156
174
|
} = {}) => {
|
|
157
175
|
const runExclusive =
|
|
158
176
|
typeof store.runExclusive === "function"
|
|
@@ -161,13 +179,14 @@ const createLoginThrottle = ({
|
|
|
161
179
|
|
|
162
180
|
const getOrCreateLoginAttemptState = (clientKey, now) =>
|
|
163
181
|
runExclusive(() => {
|
|
164
|
-
const clientStateKey = getClientStateKey(clientKey);
|
|
182
|
+
const clientStateKey = getClientStateKey(clientKey, scope);
|
|
183
|
+
const globalStateKey = getGlobalStateKey(scope);
|
|
165
184
|
return {
|
|
166
185
|
clientKey,
|
|
167
186
|
clientStateKey,
|
|
168
|
-
globalStateKey
|
|
187
|
+
globalStateKey,
|
|
169
188
|
client: getOrCreateState(store, clientStateKey, now),
|
|
170
|
-
global: getOrCreateState(store,
|
|
189
|
+
global: getOrCreateState(store, globalStateKey, now),
|
|
171
190
|
};
|
|
172
191
|
});
|
|
173
192
|
|
|
@@ -175,19 +194,19 @@ const createLoginThrottle = ({
|
|
|
175
194
|
runExclusive(() => {
|
|
176
195
|
const clientStateKey =
|
|
177
196
|
stateBundle?.clientStateKey ||
|
|
178
|
-
getClientStateKey(stateBundle?.clientKey);
|
|
179
|
-
const globalStateKey = stateBundle?.globalStateKey ||
|
|
197
|
+
getClientStateKey(stateBundle?.clientKey, scope);
|
|
198
|
+
const globalStateKey = stateBundle?.globalStateKey || getGlobalStateKey(scope);
|
|
180
199
|
const clientResult = evaluateState({
|
|
181
200
|
store,
|
|
182
201
|
stateKey: clientStateKey,
|
|
183
202
|
now,
|
|
184
|
-
windowMs
|
|
203
|
+
windowMs,
|
|
185
204
|
});
|
|
186
205
|
const globalResult = evaluateState({
|
|
187
206
|
store,
|
|
188
207
|
stateKey: globalStateKey,
|
|
189
208
|
now,
|
|
190
|
-
windowMs:
|
|
209
|
+
windowMs: globalWindowMs,
|
|
191
210
|
});
|
|
192
211
|
if (stateBundle) {
|
|
193
212
|
stateBundle.client = clientResult.state;
|
|
@@ -200,25 +219,25 @@ const createLoginThrottle = ({
|
|
|
200
219
|
runExclusive(() => {
|
|
201
220
|
const clientStateKey =
|
|
202
221
|
stateBundle?.clientStateKey ||
|
|
203
|
-
getClientStateKey(stateBundle?.clientKey);
|
|
204
|
-
const globalStateKey = stateBundle?.globalStateKey ||
|
|
222
|
+
getClientStateKey(stateBundle?.clientKey, scope);
|
|
223
|
+
const globalStateKey = stateBundle?.globalStateKey || getGlobalStateKey(scope);
|
|
205
224
|
const clientResult = recordStateFailure({
|
|
206
225
|
store,
|
|
207
226
|
stateKey: clientStateKey,
|
|
208
227
|
now,
|
|
209
|
-
windowMs
|
|
210
|
-
maxAttempts
|
|
211
|
-
baseLockMs
|
|
212
|
-
maxLockMs
|
|
228
|
+
windowMs,
|
|
229
|
+
maxAttempts,
|
|
230
|
+
baseLockMs,
|
|
231
|
+
maxLockMs,
|
|
213
232
|
});
|
|
214
233
|
const globalResult = recordStateFailure({
|
|
215
234
|
store,
|
|
216
235
|
stateKey: globalStateKey,
|
|
217
236
|
now,
|
|
218
|
-
windowMs:
|
|
219
|
-
maxAttempts:
|
|
220
|
-
baseLockMs:
|
|
221
|
-
maxLockMs:
|
|
237
|
+
windowMs: globalWindowMs,
|
|
238
|
+
maxAttempts: globalMaxAttempts,
|
|
239
|
+
baseLockMs: globalBaseLockMs,
|
|
240
|
+
maxLockMs: globalMaxLockMs,
|
|
222
241
|
});
|
|
223
242
|
if (stateBundle) {
|
|
224
243
|
stateBundle.client = clientResult.state;
|
|
@@ -230,8 +249,8 @@ const createLoginThrottle = ({
|
|
|
230
249
|
const recordLoginSuccess = (clientKey) => {
|
|
231
250
|
if (!clientKey) return;
|
|
232
251
|
runExclusive(() => {
|
|
233
|
-
store.delete(getClientStateKey(clientKey));
|
|
234
|
-
store.delete(
|
|
252
|
+
store.delete(getClientStateKey(clientKey, scope));
|
|
253
|
+
store.delete(getGlobalStateKey(scope));
|
|
235
254
|
});
|
|
236
255
|
};
|
|
237
256
|
|
|
@@ -245,7 +264,7 @@ const createLoginThrottle = ({
|
|
|
245
264
|
}
|
|
246
265
|
const state = normalizeState(rawState, now);
|
|
247
266
|
if (state.lockUntil > now) continue;
|
|
248
|
-
if (now - state.lastSeenAt >
|
|
267
|
+
if (now - state.lastSeenAt > stateTtlMs) {
|
|
249
268
|
store.delete(stateKey);
|
|
250
269
|
}
|
|
251
270
|
}
|
|
@@ -4,6 +4,7 @@ const {
|
|
|
4
4
|
ensurePluginAllowed,
|
|
5
5
|
ensureUsageTrackerPluginEntry,
|
|
6
6
|
} = require("../usage-tracker-config");
|
|
7
|
+
const { isOpenAiCompatApiEnabled } = require("../alphaclaw-config");
|
|
7
8
|
|
|
8
9
|
const kDefaultToolsProfile = "full";
|
|
9
10
|
const kBootstrapExtraFiles = [
|
|
@@ -133,16 +134,29 @@ const buildOnboardArgs = ({
|
|
|
133
134
|
return onboardArgs;
|
|
134
135
|
};
|
|
135
136
|
|
|
136
|
-
const ensureManagedConfigShell = (cfg) => {
|
|
137
|
+
const ensureManagedConfigShell = (cfg, { openAiCompatApiEnabled = false } = {}) => {
|
|
137
138
|
if (!cfg.channels) cfg.channels = {};
|
|
138
139
|
ensurePluginsShell(cfg);
|
|
139
140
|
if (!cfg.commands) cfg.commands = {};
|
|
140
141
|
if (!cfg.tools) cfg.tools = {};
|
|
142
|
+
if (!cfg.gateway) cfg.gateway = {};
|
|
141
143
|
if (!cfg.hooks) cfg.hooks = {};
|
|
142
144
|
if (!cfg.hooks.internal) cfg.hooks.internal = {};
|
|
143
145
|
if (!cfg.hooks.internal.entries) cfg.hooks.internal.entries = {};
|
|
144
146
|
cfg.commands.restart = true;
|
|
145
147
|
cfg.tools.profile = kDefaultToolsProfile;
|
|
148
|
+
if (openAiCompatApiEnabled) {
|
|
149
|
+
if (!cfg.gateway.http) cfg.gateway.http = {};
|
|
150
|
+
if (!cfg.gateway.http.endpoints) cfg.gateway.http.endpoints = {};
|
|
151
|
+
cfg.gateway.http.endpoints.chatCompletions = {
|
|
152
|
+
...(cfg.gateway.http.endpoints.chatCompletions || {}),
|
|
153
|
+
enabled: true,
|
|
154
|
+
};
|
|
155
|
+
cfg.gateway.http.endpoints.responses = {
|
|
156
|
+
...(cfg.gateway.http.endpoints.responses || {}),
|
|
157
|
+
enabled: true,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
146
160
|
cfg.hooks.internal.enabled = true;
|
|
147
161
|
cfg.hooks.internal.entries["bootstrap-extra-files"] = {
|
|
148
162
|
...(cfg.hooks.internal.entries["bootstrap-extra-files"] || {}),
|
|
@@ -224,7 +238,12 @@ const writeSanitizedOpenclawConfig = ({
|
|
|
224
238
|
}) => {
|
|
225
239
|
const configPath = `${openclawDir}/openclaw.json`;
|
|
226
240
|
const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
227
|
-
ensureManagedConfigShell(cfg
|
|
241
|
+
ensureManagedConfigShell(cfg, {
|
|
242
|
+
openAiCompatApiEnabled: isOpenAiCompatApiEnabled({
|
|
243
|
+
fsModule: fs,
|
|
244
|
+
openclawDir,
|
|
245
|
+
}),
|
|
246
|
+
});
|
|
228
247
|
applyFreshOnboardingChannels({
|
|
229
248
|
cfg,
|
|
230
249
|
varMap,
|
|
@@ -256,7 +275,12 @@ const writeManagedImportOpenclawConfig = ({
|
|
|
256
275
|
}) => {
|
|
257
276
|
const configPath = `${openclawDir}/openclaw.json`;
|
|
258
277
|
const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
259
|
-
ensureManagedConfigShell(cfg
|
|
278
|
+
ensureManagedConfigShell(cfg, {
|
|
279
|
+
openAiCompatApiEnabled: isOpenAiCompatApiEnabled({
|
|
280
|
+
fsModule: fs,
|
|
281
|
+
openclawDir,
|
|
282
|
+
}),
|
|
283
|
+
});
|
|
260
284
|
|
|
261
285
|
ensureUsageTrackerPluginEntry(cfg);
|
|
262
286
|
|
|
@@ -1,7 +1,202 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
const http = require("http");
|
|
3
|
+
const https = require("https");
|
|
4
|
+
const { URL } = require("url");
|
|
5
|
+
|
|
6
|
+
const kOpenAiCompatProxyPathPattern =
|
|
7
|
+
/^\/v1\/(?:chat\/completions|responses|embeddings|models(?:\/[^/?#]+)?)$/;
|
|
8
|
+
const kHopByHopResponseHeaders = new Set([
|
|
9
|
+
"connection",
|
|
10
|
+
"keep-alive",
|
|
11
|
+
"proxy-authenticate",
|
|
12
|
+
"proxy-authorization",
|
|
13
|
+
"te",
|
|
14
|
+
"trailer",
|
|
15
|
+
"trailers",
|
|
16
|
+
"transfer-encoding",
|
|
17
|
+
"upgrade",
|
|
18
|
+
]);
|
|
19
|
+
// Strip these even though they're not hop-by-hop: an OpenAI-compatible client
|
|
20
|
+
// (e.g. Sure's external assistant) has no business receiving cookies from the
|
|
21
|
+
// gateway, and a stray Set-Cookie crossing the AlphaClaw boundary would be a
|
|
22
|
+
// real leak.
|
|
23
|
+
const kAlwaysStrippedResponseHeaders = new Set(["set-cookie"]);
|
|
24
|
+
|
|
25
|
+
const extractBearerToken = (authorization) => {
|
|
26
|
+
const match = String(authorization || "").match(/^Bearer\s+(.+)$/i);
|
|
27
|
+
return match ? match[1].trim() : "";
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const getApiAuthThrottleState = (authThrottle, req, now) => {
|
|
31
|
+
if (!authThrottle || typeof authThrottle.getOrCreateLoginAttemptState !== "function") {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const clientKey =
|
|
35
|
+
typeof authThrottle.getClientKey === "function"
|
|
36
|
+
? authThrottle.getClientKey(req)
|
|
37
|
+
: req.ip || req.socket?.remoteAddress || "unknown";
|
|
38
|
+
return {
|
|
39
|
+
clientKey,
|
|
40
|
+
state: authThrottle.getOrCreateLoginAttemptState(clientKey, now),
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const sendTooManyAuthAttempts = (res, retryAfterSec = 1) => {
|
|
45
|
+
const normalizedRetryAfterSec = Math.max(1, Math.ceil(Number(retryAfterSec) || 1));
|
|
46
|
+
res.set("Retry-After", String(normalizedRetryAfterSec));
|
|
47
|
+
return res.status(429).json({
|
|
48
|
+
error: "Too many attempts. Try again shortly.",
|
|
49
|
+
retryAfterSec: normalizedRetryAfterSec,
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const timingSafeStringEqual = (left, right) => {
|
|
54
|
+
const leftBuffer = Buffer.from(String(left || ""), "utf8");
|
|
55
|
+
const rightBuffer = Buffer.from(String(right || ""), "utf8");
|
|
56
|
+
return (
|
|
57
|
+
leftBuffer.length === rightBuffer.length &&
|
|
58
|
+
crypto.timingSafeEqual(leftBuffer, rightBuffer)
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const extractBodyBuffer = (req) => {
|
|
63
|
+
if (Buffer.isBuffer(req.body)) return req.body;
|
|
64
|
+
if (typeof req.body === "string") return Buffer.from(req.body, "utf8");
|
|
65
|
+
if (req.body && typeof req.body === "object") {
|
|
66
|
+
return Buffer.from(JSON.stringify(req.body), "utf8");
|
|
67
|
+
}
|
|
68
|
+
return Buffer.alloc(0);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const createGatewayProxyHeaders = ({ reqHeaders, bodyBuffer }) => {
|
|
72
|
+
const headers = { ...(reqHeaders || {}) };
|
|
73
|
+
delete headers.host;
|
|
74
|
+
delete headers.connection;
|
|
75
|
+
delete headers["content-length"];
|
|
76
|
+
delete headers["transfer-encoding"];
|
|
77
|
+
// Express has already parsed and (if gzip/deflate) inflated the body, so
|
|
78
|
+
// the bytes we reserialize are plain JSON. Forwarding the original
|
|
79
|
+
// Content-Encoding would tell the gateway to gunzip plain text and fail.
|
|
80
|
+
delete headers["content-encoding"];
|
|
81
|
+
delete headers.cookie;
|
|
82
|
+
if (bodyBuffer.length > 0) {
|
|
83
|
+
headers["content-length"] = String(bodyBuffer.length);
|
|
84
|
+
if (!headers["content-type"]) headers["content-type"] = "application/json";
|
|
85
|
+
}
|
|
86
|
+
return headers;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const proxyOpenAiCompatRequest = ({
|
|
90
|
+
req,
|
|
91
|
+
res,
|
|
92
|
+
getGatewayUrl,
|
|
93
|
+
getGatewayToken,
|
|
94
|
+
openAiCompatApiThrottle,
|
|
95
|
+
}) => {
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
const throttleState = getApiAuthThrottleState(
|
|
98
|
+
openAiCompatApiThrottle,
|
|
99
|
+
req,
|
|
100
|
+
now,
|
|
101
|
+
);
|
|
102
|
+
if (
|
|
103
|
+
throttleState &&
|
|
104
|
+
typeof openAiCompatApiThrottle.evaluateLoginThrottle === "function"
|
|
105
|
+
) {
|
|
106
|
+
const throttle = openAiCompatApiThrottle.evaluateLoginThrottle(
|
|
107
|
+
throttleState.state,
|
|
108
|
+
now,
|
|
109
|
+
);
|
|
110
|
+
if (throttle.blocked) {
|
|
111
|
+
return sendTooManyAuthAttempts(res, throttle.retryAfterSec);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const bearerToken = extractBearerToken(req.headers.authorization);
|
|
116
|
+
const expectedGatewayToken = String(getGatewayToken?.() || "").trim();
|
|
117
|
+
if (
|
|
118
|
+
!bearerToken ||
|
|
119
|
+
!expectedGatewayToken ||
|
|
120
|
+
!timingSafeStringEqual(bearerToken, expectedGatewayToken)
|
|
121
|
+
) {
|
|
122
|
+
if (
|
|
123
|
+
throttleState &&
|
|
124
|
+
typeof openAiCompatApiThrottle.recordLoginFailure === "function"
|
|
125
|
+
) {
|
|
126
|
+
const failure = openAiCompatApiThrottle.recordLoginFailure(
|
|
127
|
+
throttleState.state,
|
|
128
|
+
now,
|
|
129
|
+
);
|
|
130
|
+
if (failure.locked) {
|
|
131
|
+
return sendTooManyAuthAttempts(res, failure.retryAfterSec);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return res.status(401).json({ error: "Unauthorized" });
|
|
135
|
+
}
|
|
136
|
+
if (
|
|
137
|
+
throttleState?.clientKey &&
|
|
138
|
+
typeof openAiCompatApiThrottle?.recordLoginSuccess === "function"
|
|
139
|
+
) {
|
|
140
|
+
openAiCompatApiThrottle.recordLoginSuccess(throttleState.clientKey);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let gateway;
|
|
144
|
+
try {
|
|
145
|
+
gateway = new URL(getGatewayUrl());
|
|
146
|
+
} catch {
|
|
147
|
+
return res.status(502).json({ error: "Gateway unavailable" });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const bodyBuffer = extractBodyBuffer(req);
|
|
151
|
+
const protocolClient = gateway.protocol === "https:" ? https : http;
|
|
152
|
+
const headers = createGatewayProxyHeaders({
|
|
153
|
+
reqHeaders: req.headers,
|
|
154
|
+
bodyBuffer,
|
|
155
|
+
});
|
|
156
|
+
headers.authorization = `Bearer ${bearerToken}`;
|
|
157
|
+
|
|
158
|
+
const requestOptions = {
|
|
159
|
+
protocol: gateway.protocol,
|
|
160
|
+
hostname: gateway.hostname,
|
|
161
|
+
port: gateway.port,
|
|
162
|
+
method: req.method,
|
|
163
|
+
path: req.originalUrl || req.url,
|
|
164
|
+
headers,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const proxyReq = protocolClient.request(requestOptions, (proxyRes) => {
|
|
168
|
+
res.statusCode = proxyRes.statusCode || 502;
|
|
169
|
+
for (const [key, value] of Object.entries(proxyRes.headers || {})) {
|
|
170
|
+
if (value == null) continue;
|
|
171
|
+
const lowerKey = key.toLowerCase();
|
|
172
|
+
if (kHopByHopResponseHeaders.has(lowerKey)) continue;
|
|
173
|
+
if (kAlwaysStrippedResponseHeaders.has(lowerKey)) continue;
|
|
174
|
+
res.setHeader(key, value);
|
|
175
|
+
}
|
|
176
|
+
proxyRes.pipe(res);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
proxyReq.on("error", () => {
|
|
180
|
+
if (!res.headersSent) {
|
|
181
|
+
res.status(502).json({ error: "Gateway unavailable" });
|
|
182
|
+
} else {
|
|
183
|
+
res.end();
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (bodyBuffer.length > 0) {
|
|
188
|
+
proxyReq.write(bodyBuffer);
|
|
189
|
+
}
|
|
190
|
+
proxyReq.end();
|
|
191
|
+
};
|
|
192
|
+
|
|
1
193
|
const registerProxyRoutes = ({
|
|
2
194
|
app,
|
|
3
195
|
proxy,
|
|
4
196
|
getGatewayUrl,
|
|
197
|
+
getGatewayToken,
|
|
198
|
+
isOpenAiCompatApiEnabled = () => true,
|
|
199
|
+
openAiCompatApiThrottle = null,
|
|
5
200
|
SETUP_API_PREFIXES,
|
|
6
201
|
requireAuth,
|
|
7
202
|
oauthCallbackMiddleware,
|
|
@@ -29,10 +224,33 @@ const registerProxyRoutes = ({
|
|
|
29
224
|
app.all(kHooksPathPattern, webhookMiddleware);
|
|
30
225
|
app.all(kWebhookPathPattern, webhookMiddleware);
|
|
31
226
|
|
|
227
|
+
app.all(kOpenAiCompatProxyPathPattern, (req, res) => {
|
|
228
|
+
if (!isOpenAiCompatApiEnabled()) {
|
|
229
|
+
return res.status(404).json({ error: "Not found" });
|
|
230
|
+
}
|
|
231
|
+
return proxyOpenAiCompatRequest({
|
|
232
|
+
req,
|
|
233
|
+
res,
|
|
234
|
+
getGatewayUrl,
|
|
235
|
+
getGatewayToken,
|
|
236
|
+
openAiCompatApiThrottle,
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
32
240
|
app.all(kApiPathPattern, (req, res, next) => {
|
|
33
241
|
if (SETUP_API_PREFIXES.some((p) => req.path.startsWith(p))) return next();
|
|
34
242
|
proxy.web(req, res, { target: getGatewayUrl() });
|
|
35
243
|
});
|
|
36
244
|
};
|
|
37
245
|
|
|
38
|
-
module.exports = {
|
|
246
|
+
module.exports = {
|
|
247
|
+
kOpenAiCompatProxyPathPattern,
|
|
248
|
+
registerProxyRoutes,
|
|
249
|
+
// Exported for tests.
|
|
250
|
+
__testing: {
|
|
251
|
+
createGatewayProxyHeaders,
|
|
252
|
+
extractBearerToken,
|
|
253
|
+
kHopByHopResponseHeaders,
|
|
254
|
+
kAlwaysStrippedResponseHeaders,
|
|
255
|
+
},
|
|
256
|
+
};
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
const { buildManagedPaths } = require("../internal-files-migration");
|
|
2
2
|
const { readOpenclawConfig } = require("../openclaw-config");
|
|
3
|
+
const {
|
|
4
|
+
readAlphaclawConfig,
|
|
5
|
+
updateOpenAiCompatApiFeature,
|
|
6
|
+
} = require("../alphaclaw-config");
|
|
3
7
|
const https = require("https");
|
|
4
8
|
|
|
5
9
|
const registerSystemRoutes = ({
|
|
@@ -26,6 +30,8 @@ const registerSystemRoutes = ({
|
|
|
26
30
|
authProfiles,
|
|
27
31
|
watchdog,
|
|
28
32
|
doctorService,
|
|
33
|
+
ensureGatewayProxyConfig,
|
|
34
|
+
getBaseUrl,
|
|
29
35
|
}) => {
|
|
30
36
|
let envRestartPending = false;
|
|
31
37
|
let openclawSecretRuntimePromise = null;
|
|
@@ -590,6 +596,10 @@ const registerSystemRoutes = ({
|
|
|
590
596
|
repo,
|
|
591
597
|
openclawVersion,
|
|
592
598
|
alphaclawVersion,
|
|
599
|
+
alphaclaw: readAlphaclawConfig({
|
|
600
|
+
fsModule: fs,
|
|
601
|
+
openclawDir: OPENCLAW_DIR,
|
|
602
|
+
}),
|
|
593
603
|
syncCron: getSystemCronStatus(),
|
|
594
604
|
};
|
|
595
605
|
};
|
|
@@ -668,6 +678,57 @@ const registerSystemRoutes = ({
|
|
|
668
678
|
res.json({ ok: true, syncCron: status });
|
|
669
679
|
});
|
|
670
680
|
|
|
681
|
+
app.get("/api/alphaclaw/config", (req, res) => {
|
|
682
|
+
res.json({
|
|
683
|
+
ok: true,
|
|
684
|
+
config: readAlphaclawConfig({
|
|
685
|
+
fsModule: fs,
|
|
686
|
+
openclawDir: OPENCLAW_DIR,
|
|
687
|
+
}),
|
|
688
|
+
});
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
app.put("/api/alphaclaw/config/features/openai-compat-api", async (req, res) => {
|
|
692
|
+
const { enabled } = req.body || {};
|
|
693
|
+
if (typeof enabled !== "boolean") {
|
|
694
|
+
return res
|
|
695
|
+
.status(400)
|
|
696
|
+
.json({ ok: false, error: "enabled must be a boolean" });
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
try {
|
|
700
|
+
const { config, changed } = updateOpenAiCompatApiFeature({
|
|
701
|
+
fsModule: fs,
|
|
702
|
+
openclawDir: OPENCLAW_DIR,
|
|
703
|
+
enabled,
|
|
704
|
+
});
|
|
705
|
+
let gatewayConfigChanged = false;
|
|
706
|
+
if (enabled && isOnboarded() && typeof ensureGatewayProxyConfig === "function") {
|
|
707
|
+
gatewayConfigChanged = ensureGatewayProxyConfig(getBaseUrl?.(req));
|
|
708
|
+
if (gatewayConfigChanged && restartRequiredState?.markRequired) {
|
|
709
|
+
restartRequiredState.markRequired("openai_compat_api_enabled");
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
const snapshot =
|
|
713
|
+
typeof restartRequiredState?.getSnapshot === "function"
|
|
714
|
+
? await restartRequiredState.getSnapshot()
|
|
715
|
+
: null;
|
|
716
|
+
res.json({
|
|
717
|
+
ok: true,
|
|
718
|
+
changed,
|
|
719
|
+
gatewayConfigChanged,
|
|
720
|
+
config,
|
|
721
|
+
restartRequired:
|
|
722
|
+
Boolean(snapshot?.restartRequired) || (envRestartPending && isOnboarded()),
|
|
723
|
+
});
|
|
724
|
+
} catch (err) {
|
|
725
|
+
res.status(500).json({
|
|
726
|
+
ok: false,
|
|
727
|
+
error: err.message || "Could not update AlphaClaw config",
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
|
|
671
732
|
app.get("/api/alphaclaw/version", async (req, res) => {
|
|
672
733
|
const refresh = String(req.query.refresh || "") === "1";
|
|
673
734
|
const status = await alphaclawVersionService.getVersionStatus(refresh);
|
package/lib/server.js
CHANGED
|
@@ -148,6 +148,9 @@ const {
|
|
|
148
148
|
const {
|
|
149
149
|
ensureOpenclawStartupEnv,
|
|
150
150
|
} = require("./server/openclaw-runtime-env");
|
|
151
|
+
const {
|
|
152
|
+
isOpenAiCompatApiEnabled,
|
|
153
|
+
} = require("./server/alphaclaw-config");
|
|
151
154
|
|
|
152
155
|
const { PORT, kTrustProxyHops, SETUP_API_PREFIXES } = constants;
|
|
153
156
|
|
|
@@ -165,6 +168,18 @@ const app = express();
|
|
|
165
168
|
app.set("trust proxy", kTrustProxyHops);
|
|
166
169
|
app.use(["/webhook", "/hooks"], express.raw({ type: "*/*", limit: "5mb" }));
|
|
167
170
|
app.use("/gmail-pubsub", express.raw({ type: "*/*", limit: "5mb" }));
|
|
171
|
+
const openAiCompatJsonParser = express.json({ limit: "50mb" });
|
|
172
|
+
const isOpenAiCompatApiCurrentlyEnabled = () =>
|
|
173
|
+
isOpenAiCompatApiEnabled({
|
|
174
|
+
fsModule: fs,
|
|
175
|
+
openclawDir: constants.OPENCLAW_DIR,
|
|
176
|
+
});
|
|
177
|
+
app.use("/v1", (req, res, next) => {
|
|
178
|
+
if (!isOpenAiCompatApiCurrentlyEnabled()) {
|
|
179
|
+
return res.status(404).json({ error: "Not found" });
|
|
180
|
+
}
|
|
181
|
+
return openAiCompatJsonParser(req, res, next);
|
|
182
|
+
});
|
|
168
183
|
app.use(express.json({ limit: "5mb" }));
|
|
169
184
|
|
|
170
185
|
const proxy = httpProxy.createProxyServer({
|
|
@@ -190,8 +205,25 @@ const agentsService = createAgentsService({
|
|
|
190
205
|
restartGateway: () => restartGatewayWithReload(reloadEnv),
|
|
191
206
|
clawCmd,
|
|
192
207
|
});
|
|
208
|
+
const loginThrottleStore = createLoginThrottleStore();
|
|
193
209
|
const loginThrottle = {
|
|
194
|
-
...createLoginThrottle({ store:
|
|
210
|
+
...createLoginThrottle({ store: loginThrottleStore }),
|
|
211
|
+
getClientKey,
|
|
212
|
+
};
|
|
213
|
+
const openAiCompatApiThrottle = {
|
|
214
|
+
...createLoginThrottle({
|
|
215
|
+
store: loginThrottleStore,
|
|
216
|
+
scope: "openai-compat-api",
|
|
217
|
+
windowMs: constants.kOpenAiCompatApiRateWindowMs,
|
|
218
|
+
maxAttempts: constants.kOpenAiCompatApiRateMaxAttempts,
|
|
219
|
+
baseLockMs: constants.kOpenAiCompatApiRateBaseLockMs,
|
|
220
|
+
maxLockMs: constants.kOpenAiCompatApiRateMaxLockMs,
|
|
221
|
+
globalWindowMs: constants.kOpenAiCompatApiRateGlobalWindowMs,
|
|
222
|
+
globalMaxAttempts: constants.kOpenAiCompatApiRateGlobalMaxAttempts,
|
|
223
|
+
globalBaseLockMs: constants.kOpenAiCompatApiRateGlobalBaseLockMs,
|
|
224
|
+
globalMaxLockMs: constants.kOpenAiCompatApiRateGlobalMaxLockMs,
|
|
225
|
+
stateTtlMs: constants.kOpenAiCompatApiRateStateTtlMs,
|
|
226
|
+
}),
|
|
195
227
|
getClientKey,
|
|
196
228
|
};
|
|
197
229
|
const resolveSetupUrl = () =>
|
|
@@ -318,6 +350,8 @@ const {
|
|
|
318
350
|
resolveGithubRepoUrl,
|
|
319
351
|
resolveModelProvider,
|
|
320
352
|
ensureGatewayProxyConfig,
|
|
353
|
+
isOpenAiCompatApiEnabled: isOpenAiCompatApiCurrentlyEnabled,
|
|
354
|
+
openAiCompatApiThrottle,
|
|
321
355
|
getBaseUrl,
|
|
322
356
|
startGateway,
|
|
323
357
|
ensureManagedExecDefaults: () =>
|