@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
package/lib/server/gateway.js
CHANGED
|
@@ -11,7 +11,12 @@ const {
|
|
|
11
11
|
kOnboardingMarkerPath,
|
|
12
12
|
kRootDir,
|
|
13
13
|
} = require("./constants");
|
|
14
|
+
const {
|
|
15
|
+
normalizeChannelAccountId,
|
|
16
|
+
readPairedCountsByAccount,
|
|
17
|
+
} = require("./agents/shared");
|
|
14
18
|
const { withOpenclawStartupEnv } = require("./openclaw-runtime-env");
|
|
19
|
+
const { isOpenAiCompatApiEnabled } = require("./alphaclaw-config");
|
|
15
20
|
|
|
16
21
|
let gatewayChild = null;
|
|
17
22
|
let gatewayExitHandler = null;
|
|
@@ -202,18 +207,6 @@ const getGatewayPort = () => {
|
|
|
202
207
|
|
|
203
208
|
const getGatewayUrl = () => `http://${GATEWAY_HOST}:${getGatewayPort()}`;
|
|
204
209
|
|
|
205
|
-
const normalizeChannelAccountId = (value) => String(value || "").trim() || "default";
|
|
206
|
-
|
|
207
|
-
const resolveCredentialPairingAccountId = ({ channel, fileName }) => {
|
|
208
|
-
const prefix = `${String(channel || "").trim()}-`;
|
|
209
|
-
const suffix = "-allowFrom.json";
|
|
210
|
-
if (!String(fileName || "").startsWith(prefix) || !String(fileName || "").endsWith(suffix)) {
|
|
211
|
-
return "";
|
|
212
|
-
}
|
|
213
|
-
const rawAccountId = String(fileName || "").slice(prefix.length, -suffix.length);
|
|
214
|
-
return normalizeChannelAccountId(rawAccountId);
|
|
215
|
-
};
|
|
216
|
-
|
|
217
210
|
const isGatewayRunning = () =>
|
|
218
211
|
new Promise((resolve) => {
|
|
219
212
|
const sock = net.createConnection(getGatewayPort(), GATEWAY_HOST);
|
|
@@ -505,6 +498,31 @@ const ensureGatewayProxyConfig = (origin) => {
|
|
|
505
498
|
if (!cfg.gateway) cfg.gateway = {};
|
|
506
499
|
let changed = false;
|
|
507
500
|
|
|
501
|
+
if (isOpenAiCompatApiEnabled({ fsModule: fs, openclawDir: OPENCLAW_DIR })) {
|
|
502
|
+
if (!cfg.gateway.http) cfg.gateway.http = {};
|
|
503
|
+
if (!cfg.gateway.http.endpoints) cfg.gateway.http.endpoints = {};
|
|
504
|
+
|
|
505
|
+
const chatCompletions = cfg.gateway.http.endpoints.chatCompletions || {};
|
|
506
|
+
if (chatCompletions.enabled !== true) {
|
|
507
|
+
cfg.gateway.http.endpoints.chatCompletions = {
|
|
508
|
+
...chatCompletions,
|
|
509
|
+
enabled: true,
|
|
510
|
+
};
|
|
511
|
+
console.log("[alphaclaw] Enabled gateway OpenAI chat completions endpoint");
|
|
512
|
+
changed = true;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const responses = cfg.gateway.http.endpoints.responses || {};
|
|
516
|
+
if (responses.enabled !== true) {
|
|
517
|
+
cfg.gateway.http.endpoints.responses = {
|
|
518
|
+
...responses,
|
|
519
|
+
enabled: true,
|
|
520
|
+
};
|
|
521
|
+
console.log("[alphaclaw] Enabled gateway OpenResponses endpoint");
|
|
522
|
+
changed = true;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
508
526
|
if (!Array.isArray(cfg.gateway.trustedProxies)) {
|
|
509
527
|
cfg.gateway.trustedProxies = [];
|
|
510
528
|
}
|
|
@@ -526,8 +544,144 @@ const ensureGatewayProxyConfig = (origin) => {
|
|
|
526
544
|
}
|
|
527
545
|
}
|
|
528
546
|
|
|
547
|
+
// Managed remote MCP server entry. Env-driven so any AlphaClaw operator
|
|
548
|
+
// (Render, Fly, fly.io-style PaaS, plain VPS) can wire OpenClaw to a
|
|
549
|
+
// remote MCP server without hand-editing /data/.openclaw/openclaw.json.
|
|
550
|
+
//
|
|
551
|
+
// REMOTE_MCP_URL upstream MCP endpoint (streamable-http).
|
|
552
|
+
// REMOTE_MCP_API_TOKEN Bearer token the remote MCP expects. Persisted
|
|
553
|
+
// as the ${REMOTE_MCP_API_TOKEN} reference, not
|
|
554
|
+
// raw, so the openclaw.json that gets
|
|
555
|
+
// git-committed never holds the plaintext.
|
|
556
|
+
// REMOTE_MCP_NAME Key under mcp.servers.<name>. Default "remote".
|
|
557
|
+
// REMOTE_MCP_PROXY_URL When set, OpenClaw connects here instead of
|
|
558
|
+
// REMOTE_MCP_URL. Intended for a same-host
|
|
559
|
+
// scanning proxy (e.g. `pipelock mcp proxy
|
|
560
|
+
// --listen ... --upstream <REMOTE_MCP_URL>`),
|
|
561
|
+
// but the implementation is proxy-agnostic.
|
|
562
|
+
// The supervisor that starts that proxy is
|
|
563
|
+
// responsible for unsetting this env var when
|
|
564
|
+
// the proxy is not running, so AlphaClaw never
|
|
565
|
+
// points OpenClaw at a dead listener.
|
|
566
|
+
const remoteMcpUrl = String(process.env.REMOTE_MCP_URL || "").trim();
|
|
567
|
+
const remoteMcpToken = String(
|
|
568
|
+
process.env.REMOTE_MCP_API_TOKEN || "",
|
|
569
|
+
).trim();
|
|
570
|
+
const remoteMcpProxyUrl = String(
|
|
571
|
+
process.env.REMOTE_MCP_PROXY_URL || "",
|
|
572
|
+
).trim();
|
|
573
|
+
const remoteMcpNameRaw = String(process.env.REMOTE_MCP_NAME || "").trim();
|
|
574
|
+
// Constrain the managed key. OpenClaw sanitizes names later for tool
|
|
575
|
+
// prefixes, but the config-key itself must be safe to use as an object
|
|
576
|
+
// key and to read back in `openclaw mcp` CLI commands. Reject names
|
|
577
|
+
// with prototype-pollution shapes, spaces, or path-like names; fall
|
|
578
|
+
// back to "remote" with a warning so a typo doesn't silently misroute.
|
|
579
|
+
const kRemoteMcpNamePattern = /^[A-Za-z0-9_-]{1,64}$/;
|
|
580
|
+
const kReservedRemoteMcpNames = new Set([
|
|
581
|
+
"__proto__",
|
|
582
|
+
"constructor",
|
|
583
|
+
"prototype",
|
|
584
|
+
]);
|
|
585
|
+
let remoteMcpName = "remote";
|
|
586
|
+
if (remoteMcpNameRaw) {
|
|
587
|
+
if (
|
|
588
|
+
kRemoteMcpNamePattern.test(remoteMcpNameRaw) &&
|
|
589
|
+
!kReservedRemoteMcpNames.has(remoteMcpNameRaw)
|
|
590
|
+
) {
|
|
591
|
+
remoteMcpName = remoteMcpNameRaw;
|
|
592
|
+
} else {
|
|
593
|
+
console.warn(
|
|
594
|
+
`[alphaclaw] REMOTE_MCP_NAME=${JSON.stringify(remoteMcpNameRaw)} is invalid (must match ${kRemoteMcpNamePattern} and not be a reserved key); falling back to "remote"`,
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
const placeholderAuth = "Bearer ${REMOTE_MCP_API_TOKEN}";
|
|
599
|
+
const desiredAuth = `Bearer ${remoteMcpToken}`;
|
|
600
|
+
const kManagedMarker = "_alphaclawManaged";
|
|
601
|
+
let mcpChanged = false;
|
|
602
|
+
|
|
603
|
+
// Clean up any managed entries left over from a prior REMOTE_MCP_NAME
|
|
604
|
+
// value. Without this, renaming REMOTE_MCP_NAME from "sure" to "notion"
|
|
605
|
+
// would leave the old "sure" entry behind, duplicating MCP tools or
|
|
606
|
+
// routing callbacks to a stale target. The marker scopes the cleanup so
|
|
607
|
+
// user-managed entries (no marker) are never touched.
|
|
608
|
+
if (cfg.mcp?.servers) {
|
|
609
|
+
for (const [key, entry] of Object.entries(cfg.mcp.servers)) {
|
|
610
|
+
if (
|
|
611
|
+
entry &&
|
|
612
|
+
typeof entry === "object" &&
|
|
613
|
+
entry[kManagedMarker] === true &&
|
|
614
|
+
key !== remoteMcpName
|
|
615
|
+
) {
|
|
616
|
+
delete cfg.mcp.servers[key];
|
|
617
|
+
mcpChanged = true;
|
|
618
|
+
console.log(
|
|
619
|
+
`[alphaclaw] Removed stale managed MCP server "${key}" (REMOTE_MCP_NAME is now "${remoteMcpName}")`,
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (remoteMcpUrl && remoteMcpToken) {
|
|
626
|
+
if (!cfg.mcp) cfg.mcp = {};
|
|
627
|
+
if (!cfg.mcp.servers) cfg.mcp.servers = {};
|
|
628
|
+
const existing = cfg.mcp.servers[remoteMcpName] || {};
|
|
629
|
+
const effectiveUrl = remoteMcpProxyUrl || remoteMcpUrl;
|
|
630
|
+
const existingHeaders = existing.headers || {};
|
|
631
|
+
const existingAuth = existingHeaders.Authorization;
|
|
632
|
+
// Only the placeholder counts as "already sanitized". A plaintext
|
|
633
|
+
// Bearer (even one that matches the current desiredAuth) must trigger a
|
|
634
|
+
// rewrite so the substitution loop below scrubs it back to the
|
|
635
|
+
// ${REMOTE_MCP_API_TOKEN} reference.
|
|
636
|
+
const authIsPlaceholder = existingAuth === placeholderAuth;
|
|
637
|
+
const hasManagedMarker = existing[kManagedMarker] === true;
|
|
638
|
+
if (
|
|
639
|
+
existing.url !== effectiveUrl ||
|
|
640
|
+
existing.transport !== "streamable-http" ||
|
|
641
|
+
!authIsPlaceholder ||
|
|
642
|
+
!hasManagedMarker
|
|
643
|
+
) {
|
|
644
|
+
cfg.mcp.servers[remoteMcpName] = {
|
|
645
|
+
...existing,
|
|
646
|
+
url: effectiveUrl,
|
|
647
|
+
transport: "streamable-http",
|
|
648
|
+
headers: {
|
|
649
|
+
...existingHeaders,
|
|
650
|
+
Authorization: desiredAuth,
|
|
651
|
+
},
|
|
652
|
+
[kManagedMarker]: true,
|
|
653
|
+
};
|
|
654
|
+
mcpChanged = true;
|
|
655
|
+
console.log(
|
|
656
|
+
`[alphaclaw] Configured remote MCP server "${remoteMcpName}" (url=${effectiveUrl}, via_proxy=${Boolean(remoteMcpProxyUrl)})`,
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
} else if (
|
|
660
|
+
cfg.mcp?.servers?.[remoteMcpName] &&
|
|
661
|
+
cfg.mcp.servers[remoteMcpName][kManagedMarker] === true
|
|
662
|
+
) {
|
|
663
|
+
delete cfg.mcp.servers[remoteMcpName];
|
|
664
|
+
mcpChanged = true;
|
|
665
|
+
console.log(
|
|
666
|
+
`[alphaclaw] Removed remote MCP server "${remoteMcpName}" entry (REMOTE_MCP_URL / REMOTE_MCP_API_TOKEN unset)`,
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
if (cfg.mcp?.servers && Object.keys(cfg.mcp.servers).length === 0) {
|
|
670
|
+
delete cfg.mcp.servers;
|
|
671
|
+
}
|
|
672
|
+
if (cfg.mcp && Object.keys(cfg.mcp).length === 0) {
|
|
673
|
+
delete cfg.mcp;
|
|
674
|
+
}
|
|
675
|
+
if (mcpChanged) changed = true;
|
|
676
|
+
|
|
529
677
|
if (changed) {
|
|
530
|
-
|
|
678
|
+
let content = JSON.stringify(cfg, null, 2);
|
|
679
|
+
if (remoteMcpToken) {
|
|
680
|
+
const jsonValue = JSON.stringify(desiredAuth);
|
|
681
|
+
const jsonPlaceholder = JSON.stringify(placeholderAuth);
|
|
682
|
+
content = content.split(jsonValue).join(jsonPlaceholder);
|
|
683
|
+
}
|
|
684
|
+
fs.writeFileSync(configPath, content);
|
|
531
685
|
}
|
|
532
686
|
return changed;
|
|
533
687
|
} catch (e) {
|
|
@@ -619,32 +773,6 @@ const getChannelStatus = () => {
|
|
|
619
773
|
);
|
|
620
774
|
const credDir = `${OPENCLAW_DIR}/credentials`;
|
|
621
775
|
const channels = {};
|
|
622
|
-
const hasImplicitWhatsAppSelfPairing = ({ accountId, accountConfig }) => {
|
|
623
|
-
if (!accountConfig || typeof accountConfig !== "object") return false;
|
|
624
|
-
if (accountConfig.selfChatMode === false) return false;
|
|
625
|
-
if (String(accountConfig.dmPolicy || "").trim().toLowerCase() === "disabled") {
|
|
626
|
-
return false;
|
|
627
|
-
}
|
|
628
|
-
const candidatePaths = [
|
|
629
|
-
`${credDir}/whatsapp/${accountId}/creds.json`,
|
|
630
|
-
...(accountId === "default" ? [`${credDir}/creds.json`] : []),
|
|
631
|
-
];
|
|
632
|
-
const matches = candidatePaths.map((targetPath) => {
|
|
633
|
-
try {
|
|
634
|
-
return {
|
|
635
|
-
path: targetPath,
|
|
636
|
-
exists: !!String(fs.readFileSync(targetPath, "utf8") || "").trim(),
|
|
637
|
-
};
|
|
638
|
-
} catch (error) {
|
|
639
|
-
return {
|
|
640
|
-
path: targetPath,
|
|
641
|
-
exists: false,
|
|
642
|
-
error: String(error?.message || error || "read failed"),
|
|
643
|
-
};
|
|
644
|
-
}
|
|
645
|
-
});
|
|
646
|
-
return matches.some((entry) => entry.exists);
|
|
647
|
-
};
|
|
648
776
|
|
|
649
777
|
for (const ch of Object.keys(kChannelDefs)) {
|
|
650
778
|
const channelConfig =
|
|
@@ -674,55 +802,14 @@ const getChannelStatus = () => {
|
|
|
674
802
|
});
|
|
675
803
|
if (!hasConfiguredToken) continue;
|
|
676
804
|
|
|
677
|
-
const pairedByAccount =
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
(f) => f.startsWith(`${ch}-`) && f.endsWith("-allowFrom.json"),
|
|
686
|
-
);
|
|
687
|
-
for (const file of files) {
|
|
688
|
-
const accountId = resolveCredentialPairingAccountId({
|
|
689
|
-
channel: ch,
|
|
690
|
-
fileName: file,
|
|
691
|
-
});
|
|
692
|
-
if (!accountId || !configuredAccountIds.has(accountId)) continue;
|
|
693
|
-
const data = JSON.parse(
|
|
694
|
-
fs.readFileSync(`${credDir}/${file}`, "utf8"),
|
|
695
|
-
);
|
|
696
|
-
const nextCount =
|
|
697
|
-
Number(pairedByAccount.get(accountId) || 0)
|
|
698
|
-
+ (Array.isArray(data.allowFrom) ? data.allowFrom.length : 0);
|
|
699
|
-
pairedByAccount.set(accountId, nextCount);
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
} catch {}
|
|
703
|
-
for (const [accountId, accountConfig] of accountEntries) {
|
|
704
|
-
if (ch === "whatsapp") continue;
|
|
705
|
-
const inlineAllowFrom = accountConfig?.allowFrom;
|
|
706
|
-
if (!Array.isArray(inlineAllowFrom)) continue;
|
|
707
|
-
const normalizedAccountId = normalizeChannelAccountId(accountId);
|
|
708
|
-
const nextCount =
|
|
709
|
-
Number(pairedByAccount.get(normalizedAccountId) || 0) + inlineAllowFrom.length;
|
|
710
|
-
pairedByAccount.set(normalizedAccountId, nextCount);
|
|
711
|
-
}
|
|
712
|
-
if (ch === "whatsapp") {
|
|
713
|
-
for (const [accountId, accountConfig] of accountEntries) {
|
|
714
|
-
const normalizedAccountId = normalizeChannelAccountId(accountId);
|
|
715
|
-
if (Number(pairedByAccount.get(normalizedAccountId) || 0) > 0) continue;
|
|
716
|
-
if (
|
|
717
|
-
hasImplicitWhatsAppSelfPairing({
|
|
718
|
-
accountId: normalizedAccountId,
|
|
719
|
-
accountConfig,
|
|
720
|
-
})
|
|
721
|
-
) {
|
|
722
|
-
pairedByAccount.set(normalizedAccountId, 1);
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
}
|
|
805
|
+
const pairedByAccount = readPairedCountsByAccount({
|
|
806
|
+
fsImpl: fs,
|
|
807
|
+
OPENCLAW_DIR,
|
|
808
|
+
channelId: ch,
|
|
809
|
+
accountIds: Array.from(configuredAccountIds),
|
|
810
|
+
config: channelConfig,
|
|
811
|
+
});
|
|
812
|
+
|
|
726
813
|
const accounts = Object.fromEntries(
|
|
727
814
|
Array.from(pairedByAccount.entries()).map(([accountId, paired]) => [
|
|
728
815
|
accountId,
|
|
@@ -42,6 +42,8 @@ const registerServerRoutes = ({
|
|
|
42
42
|
resolveGithubRepoUrl,
|
|
43
43
|
resolveModelProvider,
|
|
44
44
|
ensureGatewayProxyConfig,
|
|
45
|
+
isOpenAiCompatApiEnabled,
|
|
46
|
+
openAiCompatApiThrottle,
|
|
45
47
|
getBaseUrl,
|
|
46
48
|
startGateway,
|
|
47
49
|
ensureManagedExecDefaults,
|
|
@@ -133,6 +135,8 @@ const registerServerRoutes = ({
|
|
|
133
135
|
authProfiles,
|
|
134
136
|
watchdog,
|
|
135
137
|
doctorService,
|
|
138
|
+
ensureGatewayProxyConfig,
|
|
139
|
+
getBaseUrl,
|
|
136
140
|
});
|
|
137
141
|
registerBrowseRoutes({
|
|
138
142
|
app,
|
|
@@ -274,6 +278,10 @@ const registerServerRoutes = ({
|
|
|
274
278
|
app,
|
|
275
279
|
proxy,
|
|
276
280
|
getGatewayUrl,
|
|
281
|
+
getGatewayToken: () =>
|
|
282
|
+
process.env.OPENCLAW_GATEWAY_TOKEN || constants.GATEWAY_TOKEN || "",
|
|
283
|
+
isOpenAiCompatApiEnabled,
|
|
284
|
+
openAiCompatApiThrottle,
|
|
277
285
|
SETUP_API_PREFIXES,
|
|
278
286
|
requireAuth,
|
|
279
287
|
oauthCallbackMiddleware,
|
|
@@ -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
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const path = require("path");
|
|
2
2
|
const { kSetupDir } = require("../constants");
|
|
3
3
|
const { buildManagedPaths } = require("../internal-files-migration");
|
|
4
|
+
const { shouldSkipSystemCronInstall } = require("../../cli/git-runtime");
|
|
4
5
|
|
|
5
6
|
const kHourlyGitSyncTemplatePath = path.join(kSetupDir, "hourly-git-sync.sh");
|
|
6
7
|
const kSystemCronPath = "/etc/cron.d/openclaw-hourly-sync";
|
|
@@ -37,6 +38,13 @@ const installHourlyGitSyncCron = async ({ fs, openclawDir }) => {
|
|
|
37
38
|
fs.mkdirSync(configDir, { recursive: true });
|
|
38
39
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
39
40
|
|
|
41
|
+
if (shouldSkipSystemCronInstall()) {
|
|
42
|
+
console.log(
|
|
43
|
+
"[onboard] System cron install skipped by ALPHACLAW_SKIP_SYSTEM_CRON_INSTALL",
|
|
44
|
+
);
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
40
48
|
const cronContent = buildSystemCronFile({
|
|
41
49
|
schedule: config.schedule,
|
|
42
50
|
scriptPath: hourlyGitSyncPath,
|
|
@@ -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
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const isRecord = (value) =>
|
|
2
|
+
!!value && typeof value === "object" && !Array.isArray(value);
|
|
3
|
+
|
|
4
|
+
const parseStreamingMode = (value) => {
|
|
5
|
+
if (typeof value === "boolean") return value ? "partial" : "off";
|
|
6
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
7
|
+
return ["off", "partial", "block", "progress"].includes(normalized)
|
|
8
|
+
? normalized
|
|
9
|
+
: null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const migrateLegacyTelegramStreamingEntry = (entry) => {
|
|
13
|
+
if (!isRecord(entry)) return false;
|
|
14
|
+
|
|
15
|
+
const legacyStreaming = entry.streaming;
|
|
16
|
+
const hasLegacyStreaming =
|
|
17
|
+
typeof legacyStreaming === "boolean" || typeof legacyStreaming === "string";
|
|
18
|
+
const hasLegacyFields =
|
|
19
|
+
entry.streamMode !== undefined ||
|
|
20
|
+
entry.chunkMode !== undefined ||
|
|
21
|
+
entry.blockStreaming !== undefined ||
|
|
22
|
+
entry.blockStreamingCoalesce !== undefined ||
|
|
23
|
+
entry.draftChunk !== undefined;
|
|
24
|
+
if (!hasLegacyStreaming && !hasLegacyFields) return false;
|
|
25
|
+
|
|
26
|
+
const streaming = isRecord(legacyStreaming) ? { ...legacyStreaming } : {};
|
|
27
|
+
const resolvedMode =
|
|
28
|
+
parseStreamingMode(isRecord(legacyStreaming) ? legacyStreaming.mode : legacyStreaming) ||
|
|
29
|
+
parseStreamingMode(entry.streamMode) ||
|
|
30
|
+
"partial";
|
|
31
|
+
if (streaming.mode === undefined) streaming.mode = resolvedMode;
|
|
32
|
+
|
|
33
|
+
if (entry.chunkMode !== undefined && streaming.chunkMode === undefined) {
|
|
34
|
+
streaming.chunkMode = entry.chunkMode;
|
|
35
|
+
}
|
|
36
|
+
if (entry.draftChunk !== undefined) {
|
|
37
|
+
const preview = isRecord(streaming.preview) ? { ...streaming.preview } : {};
|
|
38
|
+
if (preview.chunk === undefined) preview.chunk = entry.draftChunk;
|
|
39
|
+
streaming.preview = preview;
|
|
40
|
+
}
|
|
41
|
+
if (entry.blockStreaming !== undefined || entry.blockStreamingCoalesce !== undefined) {
|
|
42
|
+
const block = isRecord(streaming.block) ? { ...streaming.block } : {};
|
|
43
|
+
if (entry.blockStreaming !== undefined && block.enabled === undefined) {
|
|
44
|
+
block.enabled = entry.blockStreaming;
|
|
45
|
+
}
|
|
46
|
+
if (entry.blockStreamingCoalesce !== undefined && block.coalesce === undefined) {
|
|
47
|
+
block.coalesce = entry.blockStreamingCoalesce;
|
|
48
|
+
}
|
|
49
|
+
streaming.block = block;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
entry.streaming = streaming;
|
|
53
|
+
delete entry.streamMode;
|
|
54
|
+
delete entry.chunkMode;
|
|
55
|
+
delete entry.blockStreaming;
|
|
56
|
+
delete entry.blockStreamingCoalesce;
|
|
57
|
+
delete entry.draftChunk;
|
|
58
|
+
return true;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const migrateLegacyTelegramStreamingConfig = (cfg = {}) => {
|
|
62
|
+
const telegram = cfg.channels?.telegram;
|
|
63
|
+
if (!isRecord(telegram)) return false;
|
|
64
|
+
|
|
65
|
+
let changed = migrateLegacyTelegramStreamingEntry(telegram);
|
|
66
|
+
if (isRecord(telegram.accounts)) {
|
|
67
|
+
for (const account of Object.values(telegram.accounts)) {
|
|
68
|
+
if (migrateLegacyTelegramStreamingEntry(account)) changed = true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return changed;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
migrateLegacyTelegramStreamingConfig,
|
|
76
|
+
migrateLegacyTelegramStreamingEntry,
|
|
77
|
+
};
|
|
@@ -27,6 +27,31 @@ const loadThinkingModule = async () => {
|
|
|
27
27
|
return thinkingModulePromise;
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
const isNormalizeThinkLevel = (candidate) => {
|
|
31
|
+
if (typeof candidate !== "function") return false;
|
|
32
|
+
try {
|
|
33
|
+
return (
|
|
34
|
+
candidate("off") === "off" &&
|
|
35
|
+
candidate("low") === "low" &&
|
|
36
|
+
candidate("high") === "high"
|
|
37
|
+
);
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const resolveNormalizeThinkLevel = (mod) => {
|
|
44
|
+
const candidates = [
|
|
45
|
+
mod.normalizeThinkLevel,
|
|
46
|
+
mod.f,
|
|
47
|
+
mod.p,
|
|
48
|
+
...Object.values(mod),
|
|
49
|
+
];
|
|
50
|
+
const match = candidates.find(isNormalizeThinkLevel);
|
|
51
|
+
if (!match) throw new Error("OpenClaw normalizeThinkLevel export not found");
|
|
52
|
+
return match;
|
|
53
|
+
};
|
|
54
|
+
|
|
30
55
|
const splitModelKey = (modelKey = "") => {
|
|
31
56
|
const normalized = String(modelKey || "").trim();
|
|
32
57
|
const slashIndex = normalized.indexOf("/");
|
|
@@ -55,7 +80,7 @@ const resolveThinkingApi = async () => {
|
|
|
55
80
|
return {
|
|
56
81
|
listThinkingLevelOptions: mod.listThinkingLevelOptions || mod.i,
|
|
57
82
|
resolveThinkingDefaultForModel: mod.resolveThinkingDefaultForModel || mod.s,
|
|
58
|
-
normalizeThinkLevel: mod
|
|
83
|
+
normalizeThinkLevel: resolveNormalizeThinkLevel(mod),
|
|
59
84
|
};
|
|
60
85
|
};
|
|
61
86
|
|