@chrysb/alphaclaw 0.9.17 → 0.9.19
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 +28 -0
- package/bin/alphaclaw.js +28 -6
- package/lib/cli/git-runtime.js +41 -0
- package/lib/cli/openclaw-config-restore.js +1 -1
- 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/scripts/git +1 -1
- package/lib/server/agents/shared.js +18 -2
- package/lib/server/alphaclaw-config.js +99 -0
- package/lib/server/constants.js +48 -0
- package/lib/server/env.js +71 -20
- package/lib/server/gateway.js +175 -88
- package/lib/server/init/register-server-routes.js +8 -0
- package/lib/server/login-throttle.js +41 -22
- package/lib/server/onboarding/cron.js +8 -0
- package/lib/server/onboarding/openclaw.js +27 -3
- package/lib/server/openclaw-config-migrations.js +77 -0
- package/lib/server/openclaw-thinking.js +26 -1
- package/lib/server/routes/proxy.js +219 -1
- package/lib/server/routes/system.js +65 -0
- package/lib/server/usage-tracker-config.js +5 -1
- package/lib/server.js +35 -1
- package/lib/setup/hourly-git-sync.sh +17 -0
- package/package.json +3 -3
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { h } from "preact";
|
|
2
|
+
import htm from "htm";
|
|
3
|
+
import { copyTextToClipboard } from "../lib/clipboard.js";
|
|
4
|
+
import { showToast } from "./toast.js";
|
|
5
|
+
import { FileCopyLineIcon } from "./icons.js";
|
|
6
|
+
import { InfoTooltip } from "./info-tooltip.js";
|
|
7
|
+
import { ToggleSwitch } from "./toggle-switch.js";
|
|
8
|
+
|
|
9
|
+
const html = htm.bind(h);
|
|
10
|
+
|
|
11
|
+
const getApiUrl = () => {
|
|
12
|
+
if (typeof window === "undefined" || !window.location?.origin) return "/v1";
|
|
13
|
+
return `${window.location.origin}/v1`;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const ApiFeaturePanel = ({
|
|
17
|
+
openAiCompatApi = { enabled: false },
|
|
18
|
+
savingOpenAiCompatApi = false,
|
|
19
|
+
onToggleOpenAiCompatApi = () => {},
|
|
20
|
+
}) => {
|
|
21
|
+
const apiHydrated = openAiCompatApi?.hydrated === true;
|
|
22
|
+
const apiEnabled = openAiCompatApi?.enabled === true;
|
|
23
|
+
const apiUrl = getApiUrl();
|
|
24
|
+
const handleCopy = async () => {
|
|
25
|
+
const copied = await copyTextToClipboard(apiUrl);
|
|
26
|
+
showToast(
|
|
27
|
+
copied ? "API URL copied" : "Could not copy API URL",
|
|
28
|
+
copied ? "success" : "error",
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return html`
|
|
33
|
+
<div class="bg-surface border border-border rounded-xl p-4">
|
|
34
|
+
<div class="flex items-center justify-between gap-3">
|
|
35
|
+
<div class="flex items-center gap-1.5 min-w-0">
|
|
36
|
+
<h2 class="card-label">API</h2>
|
|
37
|
+
<${InfoTooltip}
|
|
38
|
+
text="Allows trusted server-side clients to call OpenClaw via an OpenAI compatible API."
|
|
39
|
+
widthClass="w-72"
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
<${ToggleSwitch}
|
|
43
|
+
checked=${apiEnabled}
|
|
44
|
+
disabled=${savingOpenAiCompatApi || !apiHydrated}
|
|
45
|
+
label=${savingOpenAiCompatApi
|
|
46
|
+
? "Saving..."
|
|
47
|
+
: !apiHydrated
|
|
48
|
+
? "Loading..."
|
|
49
|
+
: apiEnabled
|
|
50
|
+
? "Enabled"
|
|
51
|
+
: "Disabled"}
|
|
52
|
+
onChange=${onToggleOpenAiCompatApi}
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
${apiHydrated && apiEnabled
|
|
56
|
+
? html`
|
|
57
|
+
<div class="mt-4 text-xs text-fg-muted mb-2">OpenAI compatible URL</div>
|
|
58
|
+
<div class="flex items-center gap-2">
|
|
59
|
+
<code class="flex-1 min-w-0 bg-field border border-border rounded-lg px-3 py-2 text-xs text-body font-mono break-all">
|
|
60
|
+
${apiUrl}
|
|
61
|
+
</code>
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
class="ac-btn-secondary text-xs p-2 rounded-lg shrink-0"
|
|
65
|
+
title="Copy URL"
|
|
66
|
+
aria-label="Copy API URL"
|
|
67
|
+
onclick=${handleCopy}
|
|
68
|
+
>
|
|
69
|
+
<${FileCopyLineIcon} className="w-4 h-4" />
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
`
|
|
73
|
+
: null}
|
|
74
|
+
</div>
|
|
75
|
+
`;
|
|
76
|
+
};
|
|
@@ -8,6 +8,7 @@ import { DevicePairings } from "../device-pairings.js";
|
|
|
8
8
|
import { ActionButton } from "../action-button.js";
|
|
9
9
|
import { Google } from "../google/index.js";
|
|
10
10
|
import { Features } from "../features.js";
|
|
11
|
+
import { ApiFeaturePanel } from "../api-feature-panel.js";
|
|
11
12
|
import { GeneralDoctorWarning } from "../doctor/general-warning.js";
|
|
12
13
|
import { ChevronDownIcon } from "../icons.js";
|
|
13
14
|
import { UpdateActionButton } from "../update-action-button.js";
|
|
@@ -136,6 +137,11 @@ export const GeneralTab = ({
|
|
|
136
137
|
onRestartRequired=${onRestartRequired}
|
|
137
138
|
onOpenGmailWebhook=${onOpenGmailWebhook}
|
|
138
139
|
/>
|
|
140
|
+
<${ApiFeaturePanel}
|
|
141
|
+
openAiCompatApi=${state.openAiCompatApi}
|
|
142
|
+
savingOpenAiCompatApi=${state.savingOpenAiCompatApi}
|
|
143
|
+
onToggleOpenAiCompatApi=${actions.handleOpenAiCompatApiToggle}
|
|
144
|
+
/>
|
|
139
145
|
|
|
140
146
|
${state.repo &&
|
|
141
147
|
html`
|
|
@@ -8,14 +8,36 @@ import {
|
|
|
8
8
|
rejectDevice,
|
|
9
9
|
rejectPairing,
|
|
10
10
|
triggerWatchdogRepair,
|
|
11
|
+
updateOpenAiCompatApiFeature,
|
|
11
12
|
updateSyncCron,
|
|
12
13
|
} from "../../lib/api.js";
|
|
13
14
|
import { usePolling } from "../../hooks/usePolling.js";
|
|
15
|
+
import {
|
|
16
|
+
kOpenAiCompatApiFeatureCacheKey,
|
|
17
|
+
} from "../../lib/storage-keys.js";
|
|
14
18
|
import { showToast } from "../toast.js";
|
|
15
19
|
import { ALL_CHANNELS } from "../channels.js";
|
|
16
20
|
|
|
17
21
|
const kDefaultSyncCronSchedule = "0 * * * *";
|
|
18
22
|
|
|
23
|
+
const readCachedOpenAiCompatApi = () => {
|
|
24
|
+
try {
|
|
25
|
+
const rawValue = window.localStorage.getItem(kOpenAiCompatApiFeatureCacheKey);
|
|
26
|
+
if (rawValue === "true") return { enabled: true, hydrated: true };
|
|
27
|
+
if (rawValue === "false") return { enabled: false, hydrated: true };
|
|
28
|
+
} catch {}
|
|
29
|
+
return { enabled: false, hydrated: false };
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const writeCachedOpenAiCompatApi = (enabled) => {
|
|
33
|
+
try {
|
|
34
|
+
window.localStorage.setItem(
|
|
35
|
+
kOpenAiCompatApiFeatureCacheKey,
|
|
36
|
+
enabled ? "true" : "false",
|
|
37
|
+
);
|
|
38
|
+
} catch {}
|
|
39
|
+
};
|
|
40
|
+
|
|
19
41
|
export const useGeneralTab = ({
|
|
20
42
|
statusData = null,
|
|
21
43
|
watchdogData = null,
|
|
@@ -30,6 +52,14 @@ export const useGeneralTab = ({
|
|
|
30
52
|
const [syncCronSchedule, setSyncCronSchedule] = useState(kDefaultSyncCronSchedule);
|
|
31
53
|
const [savingSyncCron, setSavingSyncCron] = useState(false);
|
|
32
54
|
const [syncCronChoice, setSyncCronChoice] = useState(kDefaultSyncCronSchedule);
|
|
55
|
+
const [cachedOpenAiCompatApi] = useState(readCachedOpenAiCompatApi);
|
|
56
|
+
const [openAiCompatApiEnabled, setOpenAiCompatApiEnabled] = useState(
|
|
57
|
+
cachedOpenAiCompatApi.enabled,
|
|
58
|
+
);
|
|
59
|
+
const [openAiCompatApiHydrated, setOpenAiCompatApiHydrated] = useState(
|
|
60
|
+
cachedOpenAiCompatApi.hydrated,
|
|
61
|
+
);
|
|
62
|
+
const [savingOpenAiCompatApi, setSavingOpenAiCompatApi] = useState(false);
|
|
33
63
|
const [pairingStatusRefreshing, setPairingStatusRefreshing] = useState(false);
|
|
34
64
|
const [devicePollingEnabled, setDevicePollingEnabled] = useState(false);
|
|
35
65
|
const [cliAutoApproveComplete, setCliAutoApproveComplete] = useState(false);
|
|
@@ -42,6 +72,8 @@ export const useGeneralTab = ({
|
|
|
42
72
|
const channels = status?.channels ?? null;
|
|
43
73
|
const repo = status?.repo || null;
|
|
44
74
|
const syncCron = status?.syncCron || null;
|
|
75
|
+
const openAiCompatApi = status?.alphaclaw?.features?.openaiCompatApi || null;
|
|
76
|
+
const hasOpenAiCompatApiStatus = typeof openAiCompatApi?.enabled === "boolean";
|
|
45
77
|
const openclawVersion = status?.openclawVersion || null;
|
|
46
78
|
|
|
47
79
|
const hasUnpaired = ALL_CHANNELS.some((channel) => {
|
|
@@ -134,6 +166,14 @@ export const useGeneralTab = ({
|
|
|
134
166
|
);
|
|
135
167
|
}, [syncCron?.enabled, syncCron?.schedule]);
|
|
136
168
|
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
if (!hasOpenAiCompatApiStatus) return;
|
|
171
|
+
const nextEnabled = openAiCompatApi.enabled === true;
|
|
172
|
+
setOpenAiCompatApiEnabled(nextEnabled);
|
|
173
|
+
setOpenAiCompatApiHydrated(true);
|
|
174
|
+
writeCachedOpenAiCompatApi(nextEnabled);
|
|
175
|
+
}, [hasOpenAiCompatApiStatus, openAiCompatApi?.enabled]);
|
|
176
|
+
|
|
137
177
|
useEffect(
|
|
138
178
|
() => () => {
|
|
139
179
|
if (pairingRefreshTimerRef.current) {
|
|
@@ -196,6 +236,28 @@ export const useGeneralTab = ({
|
|
|
196
236
|
});
|
|
197
237
|
};
|
|
198
238
|
|
|
239
|
+
const handleOpenAiCompatApiToggle = async (enabled) => {
|
|
240
|
+
if (savingOpenAiCompatApi) return;
|
|
241
|
+
const previousEnabled = openAiCompatApiEnabled;
|
|
242
|
+
setOpenAiCompatApiEnabled(enabled);
|
|
243
|
+
setSavingOpenAiCompatApi(true);
|
|
244
|
+
try {
|
|
245
|
+
const data = await updateOpenAiCompatApiFeature(enabled);
|
|
246
|
+
if (!data.ok) {
|
|
247
|
+
throw new Error(data.error || "Could not save API setting");
|
|
248
|
+
}
|
|
249
|
+
writeCachedOpenAiCompatApi(enabled);
|
|
250
|
+
setOpenAiCompatApiHydrated(true);
|
|
251
|
+
showToast(`API ${enabled ? "enabled" : "disabled"}`, "success");
|
|
252
|
+
onRefreshStatuses();
|
|
253
|
+
} catch (err) {
|
|
254
|
+
setOpenAiCompatApiEnabled(previousEnabled);
|
|
255
|
+
showToast(err.message || "Could not save API setting", "error");
|
|
256
|
+
} finally {
|
|
257
|
+
setSavingOpenAiCompatApi(false);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
199
261
|
const handleApprove = async (id, channel, accountId = "") => {
|
|
200
262
|
try {
|
|
201
263
|
const result = await approvePairing(id, channel, accountId);
|
|
@@ -288,12 +350,18 @@ export const useGeneralTab = ({
|
|
|
288
350
|
gatewayStatus,
|
|
289
351
|
hasUnpaired,
|
|
290
352
|
openclawVersion,
|
|
353
|
+
openAiCompatApi: {
|
|
354
|
+
...(openAiCompatApi || {}),
|
|
355
|
+
enabled: openAiCompatApiEnabled,
|
|
356
|
+
hydrated: openAiCompatApiHydrated,
|
|
357
|
+
},
|
|
291
358
|
pending,
|
|
292
359
|
pairingsPolling: pairingsPoll.isPolling,
|
|
293
360
|
pairingStatusRefreshing,
|
|
294
361
|
repairingWatchdog,
|
|
295
362
|
repo,
|
|
296
363
|
savingSyncCron,
|
|
364
|
+
savingOpenAiCompatApi,
|
|
297
365
|
syncCron,
|
|
298
366
|
syncCronChoice,
|
|
299
367
|
syncCronEnabled,
|
|
@@ -306,6 +374,7 @@ export const useGeneralTab = ({
|
|
|
306
374
|
handleDeviceApprove,
|
|
307
375
|
handleDeviceReject,
|
|
308
376
|
handleOpenDashboard,
|
|
377
|
+
handleOpenAiCompatApiToggle,
|
|
309
378
|
handleReject,
|
|
310
379
|
handleSyncCronChoiceChange,
|
|
311
380
|
handleWatchdogRepair,
|
package/lib/public/js/lib/api.js
CHANGED
|
@@ -550,6 +550,25 @@ export async function updateSyncCron(payload) {
|
|
|
550
550
|
return data;
|
|
551
551
|
}
|
|
552
552
|
|
|
553
|
+
export async function updateOpenAiCompatApiFeature(enabled) {
|
|
554
|
+
const res = await authFetch("/api/alphaclaw/config/features/openai-compat-api", {
|
|
555
|
+
method: "PUT",
|
|
556
|
+
headers: { "Content-Type": "application/json" },
|
|
557
|
+
body: JSON.stringify({ enabled }),
|
|
558
|
+
});
|
|
559
|
+
const text = await res.text();
|
|
560
|
+
let data;
|
|
561
|
+
try {
|
|
562
|
+
data = text ? JSON.parse(text) : {};
|
|
563
|
+
} catch {
|
|
564
|
+
throw new Error(text || "Could not parse AlphaClaw config response");
|
|
565
|
+
}
|
|
566
|
+
if (!res.ok) {
|
|
567
|
+
throw new Error(data.error || text || `HTTP ${res.status}`);
|
|
568
|
+
}
|
|
569
|
+
return data;
|
|
570
|
+
}
|
|
571
|
+
|
|
553
572
|
export async function fetchCronJobs({ sortBy = "nextRunAtMs", sortDir = "asc" } = {}) {
|
|
554
573
|
const params = new URLSearchParams();
|
|
555
574
|
if (sortBy) params.set("sortBy", String(sortBy));
|
|
@@ -31,3 +31,7 @@ export const kAgentLastSessionKey = "alphaclaw.agent.lastSessionKey";
|
|
|
31
31
|
|
|
32
32
|
// --- Chat ---
|
|
33
33
|
export const kChatSessionDraftsStorageKey = "alphaclaw.chat.sessionDrafts";
|
|
34
|
+
|
|
35
|
+
// --- Features ---
|
|
36
|
+
export const kOpenAiCompatApiFeatureCacheKey =
|
|
37
|
+
"alphaclaw.features.openAiCompatApi.enabled";
|
package/lib/scripts/git
CHANGED
|
@@ -505,12 +505,16 @@ const readPairedCountsByAccount = ({
|
|
|
505
505
|
} catch {}
|
|
506
506
|
|
|
507
507
|
for (const accountId of counts.keys()) {
|
|
508
|
-
if (String(channelId || "").trim() === "whatsapp") continue;
|
|
509
508
|
const accountConfig =
|
|
510
509
|
accountId === "default" &&
|
|
511
510
|
!(config.accounts && typeof config.accounts === "object")
|
|
512
511
|
? config
|
|
513
512
|
: config.accounts?.[accountId] || {};
|
|
513
|
+
if (String(channelId || "").trim() === "whatsapp") {
|
|
514
|
+
if (!hasSavedWhatsAppCredentials({ fsImpl, OPENCLAW_DIR, accountId })) {
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
514
518
|
const inlineAllowFrom = accountConfig?.allowFrom;
|
|
515
519
|
if (!Array.isArray(inlineAllowFrom)) continue;
|
|
516
520
|
counts.set(
|
|
@@ -616,7 +620,15 @@ const listConfiguredChannelAccounts = ({ fsImpl, OPENCLAW_DIR, cfg }) => {
|
|
|
616
620
|
status:
|
|
617
621
|
Number(pairedCounts.get(accountId) || 0) > 0
|
|
618
622
|
? "paired"
|
|
619
|
-
:
|
|
623
|
+
: hasImplicitWhatsAppSelfPairing({
|
|
624
|
+
fsImpl,
|
|
625
|
+
OPENCLAW_DIR,
|
|
626
|
+
channelId,
|
|
627
|
+
accountId,
|
|
628
|
+
accountConfig,
|
|
629
|
+
})
|
|
630
|
+
? "paired"
|
|
631
|
+
: "configured",
|
|
620
632
|
};
|
|
621
633
|
}),
|
|
622
634
|
};
|
|
@@ -764,6 +776,10 @@ module.exports = {
|
|
|
764
776
|
resolveCredentialsDirPath,
|
|
765
777
|
resolveWhatsAppCredentialCandidatePaths,
|
|
766
778
|
hasSavedWhatsAppCredentials,
|
|
779
|
+
normalizeChannelAccountId,
|
|
780
|
+
resolveCredentialPairingAccountId,
|
|
781
|
+
hasImplicitWhatsAppSelfPairing,
|
|
782
|
+
readPairedCountsByAccount,
|
|
767
783
|
resolveAgentWorkspacePath,
|
|
768
784
|
loadConfig,
|
|
769
785
|
saveConfig,
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const kConfigFileName = "alphaclaw.json";
|
|
5
|
+
const kDefaultAlphaclawConfig = Object.freeze({
|
|
6
|
+
features: Object.freeze({
|
|
7
|
+
openaiCompatApi: Object.freeze({
|
|
8
|
+
enabled: false,
|
|
9
|
+
}),
|
|
10
|
+
}),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const resolveAlphaclawConfigPath = ({ openclawDir } = {}) =>
|
|
14
|
+
path.join(openclawDir || process.cwd(), kConfigFileName);
|
|
15
|
+
|
|
16
|
+
const normalizeOpenAiCompatApiFeature = (feature = {}) => ({
|
|
17
|
+
...(feature && typeof feature === "object" ? feature : {}),
|
|
18
|
+
enabled: feature?.enabled === true,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const normalizeAlphaclawConfig = (raw = {}) => {
|
|
22
|
+
const base = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
23
|
+
const features =
|
|
24
|
+
base.features && typeof base.features === "object" && !Array.isArray(base.features)
|
|
25
|
+
? base.features
|
|
26
|
+
: {};
|
|
27
|
+
return {
|
|
28
|
+
...base,
|
|
29
|
+
features: {
|
|
30
|
+
...features,
|
|
31
|
+
openaiCompatApi: normalizeOpenAiCompatApiFeature(features.openaiCompatApi),
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const readAlphaclawConfig = ({
|
|
37
|
+
fsModule = fs,
|
|
38
|
+
openclawDir,
|
|
39
|
+
fallback = kDefaultAlphaclawConfig,
|
|
40
|
+
} = {}) => {
|
|
41
|
+
try {
|
|
42
|
+
const configPath = resolveAlphaclawConfigPath({ openclawDir });
|
|
43
|
+
const raw = fsModule.readFileSync(configPath, "utf8");
|
|
44
|
+
return normalizeAlphaclawConfig(JSON.parse(raw));
|
|
45
|
+
} catch {
|
|
46
|
+
return normalizeAlphaclawConfig(fallback);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const writeAlphaclawConfig = ({
|
|
51
|
+
fsModule = fs,
|
|
52
|
+
openclawDir,
|
|
53
|
+
config,
|
|
54
|
+
spacing = 2,
|
|
55
|
+
} = {}) => {
|
|
56
|
+
const configPath = resolveAlphaclawConfigPath({ openclawDir });
|
|
57
|
+
fsModule.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
58
|
+
const normalized = normalizeAlphaclawConfig(config);
|
|
59
|
+
fsModule.writeFileSync(configPath, `${JSON.stringify(normalized, null, spacing)}\n`);
|
|
60
|
+
return normalized;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const isOpenAiCompatApiEnabled = (options = {}) =>
|
|
64
|
+
readAlphaclawConfig(options).features.openaiCompatApi.enabled === true;
|
|
65
|
+
|
|
66
|
+
const updateOpenAiCompatApiFeature = ({
|
|
67
|
+
fsModule = fs,
|
|
68
|
+
openclawDir,
|
|
69
|
+
enabled,
|
|
70
|
+
} = {}) => {
|
|
71
|
+
const current = readAlphaclawConfig({ fsModule, openclawDir });
|
|
72
|
+
const next = normalizeAlphaclawConfig({
|
|
73
|
+
...current,
|
|
74
|
+
features: {
|
|
75
|
+
...current.features,
|
|
76
|
+
openaiCompatApi: {
|
|
77
|
+
...current.features.openaiCompatApi,
|
|
78
|
+
enabled: enabled === true,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
const changed =
|
|
83
|
+
current.features.openaiCompatApi.enabled !== next.features.openaiCompatApi.enabled;
|
|
84
|
+
return {
|
|
85
|
+
config: writeAlphaclawConfig({ fsModule, openclawDir, config: next }),
|
|
86
|
+
changed,
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
kConfigFileName,
|
|
92
|
+
kDefaultAlphaclawConfig,
|
|
93
|
+
isOpenAiCompatApiEnabled,
|
|
94
|
+
normalizeAlphaclawConfig,
|
|
95
|
+
readAlphaclawConfig,
|
|
96
|
+
resolveAlphaclawConfigPath,
|
|
97
|
+
updateOpenAiCompatApiFeature,
|
|
98
|
+
writeAlphaclawConfig,
|
|
99
|
+
};
|
package/lib/server/constants.js
CHANGED
|
@@ -81,6 +81,45 @@ const kLoginStateTtlMs = Math.max(
|
|
|
81
81
|
),
|
|
82
82
|
kLoginMaxLockMs,
|
|
83
83
|
);
|
|
84
|
+
const kOpenAiCompatApiRateWindowMs = parsePositiveInt(
|
|
85
|
+
process.env.OPENAI_COMPAT_API_RATE_WINDOW_MS,
|
|
86
|
+
kLoginWindowMs,
|
|
87
|
+
);
|
|
88
|
+
const kOpenAiCompatApiRateMaxAttempts = parsePositiveInt(
|
|
89
|
+
process.env.OPENAI_COMPAT_API_RATE_MAX_ATTEMPTS,
|
|
90
|
+
10,
|
|
91
|
+
);
|
|
92
|
+
const kOpenAiCompatApiRateBaseLockMs = parsePositiveInt(
|
|
93
|
+
process.env.OPENAI_COMPAT_API_RATE_BASE_LOCK_MS,
|
|
94
|
+
kLoginBaseLockMs,
|
|
95
|
+
);
|
|
96
|
+
const kOpenAiCompatApiRateMaxLockMs = parsePositiveInt(
|
|
97
|
+
process.env.OPENAI_COMPAT_API_RATE_MAX_LOCK_MS,
|
|
98
|
+
kLoginMaxLockMs,
|
|
99
|
+
);
|
|
100
|
+
const kOpenAiCompatApiRateGlobalWindowMs = parsePositiveInt(
|
|
101
|
+
process.env.OPENAI_COMPAT_API_RATE_GLOBAL_WINDOW_MS,
|
|
102
|
+
kOpenAiCompatApiRateWindowMs,
|
|
103
|
+
);
|
|
104
|
+
const kOpenAiCompatApiRateGlobalMaxAttempts = parsePositiveInt(
|
|
105
|
+
process.env.OPENAI_COMPAT_API_RATE_GLOBAL_MAX_ATTEMPTS,
|
|
106
|
+
Math.max(kOpenAiCompatApiRateMaxAttempts * 10, 100),
|
|
107
|
+
);
|
|
108
|
+
const kOpenAiCompatApiRateGlobalBaseLockMs = parsePositiveInt(
|
|
109
|
+
process.env.OPENAI_COMPAT_API_RATE_GLOBAL_BASE_LOCK_MS,
|
|
110
|
+
kOpenAiCompatApiRateBaseLockMs,
|
|
111
|
+
);
|
|
112
|
+
const kOpenAiCompatApiRateGlobalMaxLockMs = parsePositiveInt(
|
|
113
|
+
process.env.OPENAI_COMPAT_API_RATE_GLOBAL_MAX_LOCK_MS,
|
|
114
|
+
kOpenAiCompatApiRateMaxLockMs,
|
|
115
|
+
);
|
|
116
|
+
const kOpenAiCompatApiRateStateTtlMs = Math.max(
|
|
117
|
+
parsePositiveInt(
|
|
118
|
+
process.env.OPENAI_COMPAT_API_RATE_STATE_TTL_MS,
|
|
119
|
+
Math.max(kOpenAiCompatApiRateWindowMs, kOpenAiCompatApiRateMaxLockMs) * 3,
|
|
120
|
+
),
|
|
121
|
+
kOpenAiCompatApiRateMaxLockMs,
|
|
122
|
+
);
|
|
84
123
|
|
|
85
124
|
const kOnboardingModelProviders = new Set([
|
|
86
125
|
"anthropic",
|
|
@@ -472,6 +511,15 @@ module.exports = {
|
|
|
472
511
|
kLoginGlobalMaxLockMs,
|
|
473
512
|
kLoginCleanupIntervalMs,
|
|
474
513
|
kLoginStateTtlMs,
|
|
514
|
+
kOpenAiCompatApiRateWindowMs,
|
|
515
|
+
kOpenAiCompatApiRateMaxAttempts,
|
|
516
|
+
kOpenAiCompatApiRateBaseLockMs,
|
|
517
|
+
kOpenAiCompatApiRateMaxLockMs,
|
|
518
|
+
kOpenAiCompatApiRateGlobalWindowMs,
|
|
519
|
+
kOpenAiCompatApiRateGlobalMaxAttempts,
|
|
520
|
+
kOpenAiCompatApiRateGlobalBaseLockMs,
|
|
521
|
+
kOpenAiCompatApiRateGlobalMaxLockMs,
|
|
522
|
+
kOpenAiCompatApiRateStateTtlMs,
|
|
475
523
|
kOnboardingModelProviders,
|
|
476
524
|
kFallbackOnboardingModels,
|
|
477
525
|
kVersionCacheTtlMs,
|
package/lib/server/env.js
CHANGED
|
@@ -1,21 +1,48 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const { ENV_FILE_PATH, kKnownVars } = require("./constants");
|
|
3
3
|
|
|
4
|
+
const kSensitiveEnvKeyPattern = /token|key|password/i;
|
|
5
|
+
const kEnvWatchDebounceMs = 250;
|
|
6
|
+
let envWatchDebounceTimer = null;
|
|
7
|
+
let lastLoadedEnvSignature = null;
|
|
8
|
+
let pendingSelfWriteSignature = null;
|
|
9
|
+
|
|
10
|
+
const normalizeEnvVars = (vars) => {
|
|
11
|
+
const byKey = new Map();
|
|
12
|
+
for (const entry of vars || []) {
|
|
13
|
+
const key = String(entry?.key || "").trim();
|
|
14
|
+
if (!key) continue;
|
|
15
|
+
if (byKey.has(key)) byKey.delete(key);
|
|
16
|
+
byKey.set(key, {
|
|
17
|
+
key,
|
|
18
|
+
value: String(entry?.value || ""),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return Array.from(byKey.values());
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const buildEnvSignature = (vars) =>
|
|
25
|
+
JSON.stringify(normalizeEnvVars(vars).map(({ key, value }) => [key, value]));
|
|
26
|
+
|
|
27
|
+
const readRawEnvFile = () => {
|
|
28
|
+
const content = fs.readFileSync(ENV_FILE_PATH, "utf8");
|
|
29
|
+
const vars = [];
|
|
30
|
+
for (const line of content.split("\n")) {
|
|
31
|
+
const trimmed = line.trim();
|
|
32
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
33
|
+
const eqIdx = trimmed.indexOf("=");
|
|
34
|
+
if (eqIdx === -1) continue;
|
|
35
|
+
vars.push({
|
|
36
|
+
key: trimmed.slice(0, eqIdx).trim(),
|
|
37
|
+
value: trimmed.slice(eqIdx + 1),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return vars;
|
|
41
|
+
};
|
|
42
|
+
|
|
4
43
|
const readEnvFile = () => {
|
|
5
44
|
try {
|
|
6
|
-
|
|
7
|
-
const vars = [];
|
|
8
|
-
for (const line of content.split("\n")) {
|
|
9
|
-
const trimmed = line.trim();
|
|
10
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
11
|
-
const eqIdx = trimmed.indexOf("=");
|
|
12
|
-
if (eqIdx === -1) continue;
|
|
13
|
-
vars.push({
|
|
14
|
-
key: trimmed.slice(0, eqIdx),
|
|
15
|
-
value: trimmed.slice(eqIdx + 1),
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
return vars;
|
|
45
|
+
return normalizeEnvVars(readRawEnvFile());
|
|
19
46
|
} catch {
|
|
20
47
|
return [];
|
|
21
48
|
}
|
|
@@ -23,22 +50,24 @@ const readEnvFile = () => {
|
|
|
23
50
|
|
|
24
51
|
const writeEnvFile = (vars) => {
|
|
25
52
|
const lines = [];
|
|
26
|
-
|
|
27
|
-
|
|
53
|
+
const normalizedVars = normalizeEnvVars(vars);
|
|
54
|
+
for (const { key, value } of normalizedVars) {
|
|
28
55
|
lines.push(`${key}=${String(value || "")}`);
|
|
29
56
|
}
|
|
30
57
|
fs.writeFileSync(ENV_FILE_PATH, lines.join("\n"));
|
|
58
|
+
pendingSelfWriteSignature = buildEnvSignature(normalizedVars);
|
|
31
59
|
};
|
|
32
60
|
|
|
33
61
|
const reloadEnv = () => {
|
|
34
62
|
const vars = readEnvFile();
|
|
63
|
+
const signature = buildEnvSignature(vars);
|
|
35
64
|
const fileKeys = new Set(vars.map((v) => v.key));
|
|
36
65
|
let changed = false;
|
|
37
66
|
|
|
38
67
|
for (const { key, value } of vars) {
|
|
39
68
|
if (value && value !== process.env[key]) {
|
|
40
69
|
console.log(
|
|
41
|
-
`[alphaclaw] Env updated: ${key}=${
|
|
70
|
+
`[alphaclaw] Env updated: ${key}=${kSensitiveEnvKeyPattern.test(key) ? "***" : value}`,
|
|
42
71
|
);
|
|
43
72
|
process.env[key] = value;
|
|
44
73
|
changed = true;
|
|
@@ -58,21 +87,43 @@ const reloadEnv = () => {
|
|
|
58
87
|
}
|
|
59
88
|
}
|
|
60
89
|
|
|
90
|
+
lastLoadedEnvSignature = signature;
|
|
61
91
|
return changed;
|
|
62
92
|
};
|
|
63
93
|
|
|
94
|
+
const readEnvFileSignature = () => {
|
|
95
|
+
try {
|
|
96
|
+
return buildEnvSignature(readRawEnvFile());
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
64
102
|
const startEnvWatcher = () => {
|
|
65
103
|
try {
|
|
66
104
|
fs.watchFile(ENV_FILE_PATH, { interval: 2000 }, () => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
105
|
+
if (envWatchDebounceTimer) clearTimeout(envWatchDebounceTimer);
|
|
106
|
+
envWatchDebounceTimer = setTimeout(() => {
|
|
107
|
+
envWatchDebounceTimer = null;
|
|
108
|
+
const signature = readEnvFileSignature();
|
|
109
|
+
if (signature && signature === pendingSelfWriteSignature) {
|
|
110
|
+
pendingSelfWriteSignature = null;
|
|
111
|
+
lastLoadedEnvSignature = signature;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
pendingSelfWriteSignature = null;
|
|
115
|
+
if (signature && signature === lastLoadedEnvSignature) return;
|
|
116
|
+
console.log(
|
|
117
|
+
`[alphaclaw] ${ENV_FILE_PATH} changed externally, reloading...`,
|
|
118
|
+
);
|
|
119
|
+
reloadEnv();
|
|
120
|
+
}, kEnvWatchDebounceMs);
|
|
71
121
|
});
|
|
72
122
|
} catch {}
|
|
73
123
|
};
|
|
74
124
|
|
|
75
125
|
module.exports = {
|
|
126
|
+
normalizeEnvVars,
|
|
76
127
|
readEnvFile,
|
|
77
128
|
writeEnvFile,
|
|
78
129
|
reloadEnv,
|