@chrysb/alphaclaw 0.9.18 → 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 +3 -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/scripts/git +1 -1
- package/lib/server/agents/shared.js +18 -2
- package/lib/server/env.js +71 -20
- package/lib/server/gateway.js +12 -87
- package/lib/server/onboarding/cron.js +8 -0
- package/lib/server/openclaw-config-migrations.js +77 -0
- package/lib/server/openclaw-thinking.js +26 -1
- package/lib/server/routes/system.js +4 -0
- package/lib/server/usage-tracker-config.js +5 -1
- package/lib/setup/hourly-git-sync.sh +17 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -157,6 +157,9 @@ The built-in watchdog monitors gateway health and recovers from failures automat
|
|
|
157
157
|
| `WATCHDOG_NOTIFICATIONS_DISABLED` | Optional | Disable watchdog notifications (`true`/`false`) |
|
|
158
158
|
| `PORT` | Optional | Server port (default `3000`) |
|
|
159
159
|
| `ALPHACLAW_ROOT_DIR` | Optional | Data directory (default `/data`) |
|
|
160
|
+
| `ALPHACLAW_SKIP_SYSTEM_CRON_INSTALL` | Optional | Skip writes to `/etc/cron.d` while keeping cron config (`true`/`false`); the managed hourly script still exits when sync is disabled |
|
|
161
|
+
| `ALPHACLAW_GIT_SHIM_PATH` | Optional | Install the managed git auth shim at this path and prepend its directory to runtime `PATH` (default `/usr/local/bin/git`) |
|
|
162
|
+
| `ALPHACLAW_GIT_ASKPASS_PATH` | Optional | Install the git askpass helper at this path (default `$TMPDIR/alphaclaw-git-askpass.sh`) |
|
|
160
163
|
| `TRUST_PROXY_HOPS` | Optional | Trust proxy hop count for correct client IP |
|
|
161
164
|
| `REMOTE_MCP_URL` | Optional | Upstream remote MCP server URL. When set together with `REMOTE_MCP_API_TOKEN`, AlphaClaw writes a managed `mcp.servers.<name>` entry to `openclaw.json` on every gateway start. |
|
|
162
165
|
| `REMOTE_MCP_API_TOKEN` | Optional | Bearer token for the remote MCP server. Persisted in `openclaw.json` as the `${REMOTE_MCP_API_TOKEN}` reference, never as plaintext. |
|
package/bin/alphaclaw.js
CHANGED
|
@@ -6,6 +6,10 @@ const os = require("os");
|
|
|
6
6
|
const path = require("path");
|
|
7
7
|
const { execSync } = require("child_process");
|
|
8
8
|
const {
|
|
9
|
+
shouldSkipSystemCronInstall,
|
|
10
|
+
resolveGitAskPassPath,
|
|
11
|
+
resolveGitShimPath,
|
|
12
|
+
prependGitShimDirToPath,
|
|
9
13
|
normalizeGitSyncFilePath,
|
|
10
14
|
validateGitSyncFilePath,
|
|
11
15
|
resolveRealGitPath,
|
|
@@ -16,6 +20,9 @@ const {
|
|
|
16
20
|
restoreMissingOpenclawConfigFromRemote,
|
|
17
21
|
} = require("../lib/cli/openclaw-config-restore");
|
|
18
22
|
const { buildSecretReplacements } = require("../lib/server/helpers");
|
|
23
|
+
const {
|
|
24
|
+
migrateLegacyTelegramStreamingConfig,
|
|
25
|
+
} = require("../lib/server/openclaw-config-migrations");
|
|
19
26
|
const {
|
|
20
27
|
migrateManagedInternalFiles,
|
|
21
28
|
} = require("../lib/server/internal-files-migration");
|
|
@@ -278,7 +285,7 @@ const runGitSync = () => {
|
|
|
278
285
|
}
|
|
279
286
|
|
|
280
287
|
const realGitPath = resolveRealGitPath({
|
|
281
|
-
shimPath:
|
|
288
|
+
shimPath: resolveGitShimPath(),
|
|
282
289
|
});
|
|
283
290
|
if (!realGitPath) {
|
|
284
291
|
console.error(
|
|
@@ -631,7 +638,11 @@ if (fs.existsSync(hourlyGitSyncPath)) {
|
|
|
631
638
|
}
|
|
632
639
|
|
|
633
640
|
const cronFilePath = "/etc/cron.d/openclaw-hourly-sync";
|
|
634
|
-
if (
|
|
641
|
+
if (shouldSkipSystemCronInstall()) {
|
|
642
|
+
console.log(
|
|
643
|
+
"[alphaclaw] System cron setup skipped by ALPHACLAW_SKIP_SYSTEM_CRON_INSTALL",
|
|
644
|
+
);
|
|
645
|
+
} else if (cronEnabled) {
|
|
635
646
|
const cronContent = [
|
|
636
647
|
"SHELL=/bin/bash",
|
|
637
648
|
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
@@ -732,7 +743,10 @@ if (fs.existsSync(configPath)) {
|
|
|
732
743
|
if (!cfg.plugins.load) cfg.plugins.load = {};
|
|
733
744
|
if (!Array.isArray(cfg.plugins.load.paths)) cfg.plugins.load.paths = [];
|
|
734
745
|
if (!cfg.plugins.entries) cfg.plugins.entries = {};
|
|
735
|
-
let changed =
|
|
746
|
+
let changed = migrateLegacyTelegramStreamingConfig(cfg);
|
|
747
|
+
if (changed) {
|
|
748
|
+
console.log("[alphaclaw] Migrated legacy Telegram streaming config");
|
|
749
|
+
}
|
|
736
750
|
|
|
737
751
|
if (process.env.TELEGRAM_BOT_TOKEN && !cfg.channels.telegram) {
|
|
738
752
|
cfg.channels.telegram = {
|
|
@@ -819,11 +833,17 @@ try {
|
|
|
819
833
|
|
|
820
834
|
try {
|
|
821
835
|
const gitAskPassSrc = path.join(__dirname, "..", "lib", "scripts", "git-askpass");
|
|
822
|
-
const gitAskPassDest =
|
|
836
|
+
const gitAskPassDest = resolveGitAskPassPath({
|
|
837
|
+
tmpDir: os.tmpdir(),
|
|
838
|
+
});
|
|
823
839
|
const gitShimTemplatePath = path.join(__dirname, "..", "lib", "scripts", "git");
|
|
824
|
-
const gitShimDest =
|
|
840
|
+
const gitShimDest = resolveGitShimPath();
|
|
841
|
+
process.env.PATH = prependGitShimDirToPath({
|
|
842
|
+
shimPath: gitShimDest,
|
|
843
|
+
});
|
|
825
844
|
|
|
826
845
|
if (fs.existsSync(gitAskPassSrc)) {
|
|
846
|
+
fs.mkdirSync(path.dirname(gitAskPassDest), { recursive: true });
|
|
827
847
|
fs.copyFileSync(gitAskPassSrc, gitAskPassDest);
|
|
828
848
|
fs.chmodSync(gitAskPassDest, 0o755);
|
|
829
849
|
}
|
|
@@ -837,7 +857,9 @@ try {
|
|
|
837
857
|
const gitShimTemplate = fs.readFileSync(gitShimTemplatePath, "utf8");
|
|
838
858
|
const gitShimContent = gitShimTemplate
|
|
839
859
|
.replace("@@REAL_GIT@@", realGitPath)
|
|
840
|
-
.replace("@@OPENCLAW_REPO_ROOT@@", openclawDir)
|
|
860
|
+
.replace("@@OPENCLAW_REPO_ROOT@@", openclawDir)
|
|
861
|
+
.replace("@@ASKPASS_PATH@@", gitAskPassDest);
|
|
862
|
+
fs.mkdirSync(path.dirname(gitShimDest), { recursive: true });
|
|
841
863
|
fs.writeFileSync(gitShimDest, gitShimContent, { mode: 0o755 });
|
|
842
864
|
console.log("[alphaclaw] git auth shim installed");
|
|
843
865
|
}
|
package/lib/cli/git-runtime.js
CHANGED
|
@@ -2,6 +2,43 @@ const fs = require("fs");
|
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const { execSync } = require("child_process");
|
|
4
4
|
|
|
5
|
+
const isEnabledEnvFlag = (value) =>
|
|
6
|
+
["1", "true", "yes", "on"].includes(
|
|
7
|
+
String(value || "").trim().toLowerCase(),
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const shouldSkipSystemCronInstall = ({ env = process.env } = {}) =>
|
|
11
|
+
isEnabledEnvFlag(env.ALPHACLAW_SKIP_SYSTEM_CRON_INSTALL);
|
|
12
|
+
|
|
13
|
+
const resolveGitAskPassPath = ({ env = process.env, tmpDir = "/tmp" } = {}) => {
|
|
14
|
+
const explicitPath = String(env.ALPHACLAW_GIT_ASKPASS_PATH || "").trim();
|
|
15
|
+
if (explicitPath) return explicitPath;
|
|
16
|
+
|
|
17
|
+
const runtimeTmpDir =
|
|
18
|
+
String(env.TMPDIR || "").trim() || String(tmpDir || "").trim() || "/tmp";
|
|
19
|
+
return path.join(runtimeTmpDir, "alphaclaw-git-askpass.sh");
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const resolveGitShimPath = ({ env = process.env } = {}) => {
|
|
23
|
+
const explicitPath = String(env.ALPHACLAW_GIT_SHIM_PATH || "").trim();
|
|
24
|
+
return explicitPath || "/usr/local/bin/git";
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const prependGitShimDirToPath = ({
|
|
28
|
+
env = process.env,
|
|
29
|
+
shimPath = resolveGitShimPath({ env }),
|
|
30
|
+
} = {}) => {
|
|
31
|
+
if (!String(env.ALPHACLAW_GIT_SHIM_PATH || "").trim()) {
|
|
32
|
+
return env.PATH || process.env.PATH || "";
|
|
33
|
+
}
|
|
34
|
+
const shimDir = path.dirname(String(shimPath || "").trim());
|
|
35
|
+
if (!shimDir || shimDir === ".") return env.PATH || process.env.PATH || "";
|
|
36
|
+
const pathEntries = String(env.PATH || process.env.PATH || "")
|
|
37
|
+
.split(path.delimiter)
|
|
38
|
+
.filter((entry) => entry && entry !== shimDir);
|
|
39
|
+
return [shimDir, ...pathEntries].join(path.delimiter);
|
|
40
|
+
};
|
|
41
|
+
|
|
5
42
|
const normalizeGitSyncFilePath = (requestedFilePath) => {
|
|
6
43
|
const rawPath = String(requestedFilePath || "").trim();
|
|
7
44
|
if (!rawPath) return "";
|
|
@@ -90,6 +127,10 @@ const shouldRefreshHourlyGitSyncScript = ({
|
|
|
90
127
|
};
|
|
91
128
|
|
|
92
129
|
module.exports = {
|
|
130
|
+
shouldSkipSystemCronInstall,
|
|
131
|
+
resolveGitAskPassPath,
|
|
132
|
+
resolveGitShimPath,
|
|
133
|
+
prependGitShimDirToPath,
|
|
93
134
|
normalizeGitSyncFilePath,
|
|
94
135
|
validateGitSyncFilePath,
|
|
95
136
|
resolveRealGitPath,
|
|
@@ -27,7 +27,7 @@ const resolveCurrentBranch = ({ execSyncImpl, openclawDir }) => {
|
|
|
27
27
|
|
|
28
28
|
const createGitEnv = ({ fsModule, osModule, env, processId }) => {
|
|
29
29
|
const githubToken = String(env.GITHUB_TOKEN || "").trim();
|
|
30
|
-
const gitEnv = { ...env };
|
|
30
|
+
const gitEnv = { ...env, PATH: env.PATH || process.env.PATH };
|
|
31
31
|
if (!githubToken) {
|
|
32
32
|
return { gitEnv, askPassPath: "" };
|
|
33
33
|
}
|
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,
|
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,
|
package/lib/server/gateway.js
CHANGED
|
@@ -11,6 +11,10 @@ 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");
|
|
15
19
|
const { isOpenAiCompatApiEnabled } = require("./alphaclaw-config");
|
|
16
20
|
|
|
@@ -203,18 +207,6 @@ const getGatewayPort = () => {
|
|
|
203
207
|
|
|
204
208
|
const getGatewayUrl = () => `http://${GATEWAY_HOST}:${getGatewayPort()}`;
|
|
205
209
|
|
|
206
|
-
const normalizeChannelAccountId = (value) => String(value || "").trim() || "default";
|
|
207
|
-
|
|
208
|
-
const resolveCredentialPairingAccountId = ({ channel, fileName }) => {
|
|
209
|
-
const prefix = `${String(channel || "").trim()}-`;
|
|
210
|
-
const suffix = "-allowFrom.json";
|
|
211
|
-
if (!String(fileName || "").startsWith(prefix) || !String(fileName || "").endsWith(suffix)) {
|
|
212
|
-
return "";
|
|
213
|
-
}
|
|
214
|
-
const rawAccountId = String(fileName || "").slice(prefix.length, -suffix.length);
|
|
215
|
-
return normalizeChannelAccountId(rawAccountId);
|
|
216
|
-
};
|
|
217
|
-
|
|
218
210
|
const isGatewayRunning = () =>
|
|
219
211
|
new Promise((resolve) => {
|
|
220
212
|
const sock = net.createConnection(getGatewayPort(), GATEWAY_HOST);
|
|
@@ -781,32 +773,6 @@ const getChannelStatus = () => {
|
|
|
781
773
|
);
|
|
782
774
|
const credDir = `${OPENCLAW_DIR}/credentials`;
|
|
783
775
|
const channels = {};
|
|
784
|
-
const hasImplicitWhatsAppSelfPairing = ({ accountId, accountConfig }) => {
|
|
785
|
-
if (!accountConfig || typeof accountConfig !== "object") return false;
|
|
786
|
-
if (accountConfig.selfChatMode === false) return false;
|
|
787
|
-
if (String(accountConfig.dmPolicy || "").trim().toLowerCase() === "disabled") {
|
|
788
|
-
return false;
|
|
789
|
-
}
|
|
790
|
-
const candidatePaths = [
|
|
791
|
-
`${credDir}/whatsapp/${accountId}/creds.json`,
|
|
792
|
-
...(accountId === "default" ? [`${credDir}/creds.json`] : []),
|
|
793
|
-
];
|
|
794
|
-
const matches = candidatePaths.map((targetPath) => {
|
|
795
|
-
try {
|
|
796
|
-
return {
|
|
797
|
-
path: targetPath,
|
|
798
|
-
exists: !!String(fs.readFileSync(targetPath, "utf8") || "").trim(),
|
|
799
|
-
};
|
|
800
|
-
} catch (error) {
|
|
801
|
-
return {
|
|
802
|
-
path: targetPath,
|
|
803
|
-
exists: false,
|
|
804
|
-
error: String(error?.message || error || "read failed"),
|
|
805
|
-
};
|
|
806
|
-
}
|
|
807
|
-
});
|
|
808
|
-
return matches.some((entry) => entry.exists);
|
|
809
|
-
};
|
|
810
776
|
|
|
811
777
|
for (const ch of Object.keys(kChannelDefs)) {
|
|
812
778
|
const channelConfig =
|
|
@@ -836,55 +802,14 @@ const getChannelStatus = () => {
|
|
|
836
802
|
});
|
|
837
803
|
if (!hasConfiguredToken) continue;
|
|
838
804
|
|
|
839
|
-
const pairedByAccount =
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
(f) => f.startsWith(`${ch}-`) && f.endsWith("-allowFrom.json"),
|
|
848
|
-
);
|
|
849
|
-
for (const file of files) {
|
|
850
|
-
const accountId = resolveCredentialPairingAccountId({
|
|
851
|
-
channel: ch,
|
|
852
|
-
fileName: file,
|
|
853
|
-
});
|
|
854
|
-
if (!accountId || !configuredAccountIds.has(accountId)) continue;
|
|
855
|
-
const data = JSON.parse(
|
|
856
|
-
fs.readFileSync(`${credDir}/${file}`, "utf8"),
|
|
857
|
-
);
|
|
858
|
-
const nextCount =
|
|
859
|
-
Number(pairedByAccount.get(accountId) || 0)
|
|
860
|
-
+ (Array.isArray(data.allowFrom) ? data.allowFrom.length : 0);
|
|
861
|
-
pairedByAccount.set(accountId, nextCount);
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
} catch {}
|
|
865
|
-
for (const [accountId, accountConfig] of accountEntries) {
|
|
866
|
-
if (ch === "whatsapp") continue;
|
|
867
|
-
const inlineAllowFrom = accountConfig?.allowFrom;
|
|
868
|
-
if (!Array.isArray(inlineAllowFrom)) continue;
|
|
869
|
-
const normalizedAccountId = normalizeChannelAccountId(accountId);
|
|
870
|
-
const nextCount =
|
|
871
|
-
Number(pairedByAccount.get(normalizedAccountId) || 0) + inlineAllowFrom.length;
|
|
872
|
-
pairedByAccount.set(normalizedAccountId, nextCount);
|
|
873
|
-
}
|
|
874
|
-
if (ch === "whatsapp") {
|
|
875
|
-
for (const [accountId, accountConfig] of accountEntries) {
|
|
876
|
-
const normalizedAccountId = normalizeChannelAccountId(accountId);
|
|
877
|
-
if (Number(pairedByAccount.get(normalizedAccountId) || 0) > 0) continue;
|
|
878
|
-
if (
|
|
879
|
-
hasImplicitWhatsAppSelfPairing({
|
|
880
|
-
accountId: normalizedAccountId,
|
|
881
|
-
accountConfig,
|
|
882
|
-
})
|
|
883
|
-
) {
|
|
884
|
-
pairedByAccount.set(normalizedAccountId, 1);
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
}
|
|
805
|
+
const pairedByAccount = readPairedCountsByAccount({
|
|
806
|
+
fsImpl: fs,
|
|
807
|
+
OPENCLAW_DIR,
|
|
808
|
+
channelId: ch,
|
|
809
|
+
accountIds: Array.from(configuredAccountIds),
|
|
810
|
+
config: channelConfig,
|
|
811
|
+
});
|
|
812
|
+
|
|
888
813
|
const accounts = Object.fromEntries(
|
|
889
814
|
Array.from(pairedByAccount.entries()).map(([accountId, paired]) => [
|
|
890
815
|
accountId,
|
|
@@ -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,
|
|
@@ -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
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { buildManagedPaths } = require("../internal-files-migration");
|
|
2
2
|
const { readOpenclawConfig } = require("../openclaw-config");
|
|
3
|
+
const { shouldSkipSystemCronInstall } = require("../../cli/git-runtime");
|
|
3
4
|
const {
|
|
4
5
|
readAlphaclawConfig,
|
|
5
6
|
updateOpenAiCompatApiFeature,
|
|
@@ -388,6 +389,9 @@ const registerSystemRoutes = ({
|
|
|
388
389
|
kSystemCronConfigPath,
|
|
389
390
|
JSON.stringify(nextConfig, null, 2),
|
|
390
391
|
);
|
|
392
|
+
if (shouldSkipSystemCronInstall()) {
|
|
393
|
+
return getSystemCronStatus();
|
|
394
|
+
}
|
|
391
395
|
if (nextConfig.enabled) {
|
|
392
396
|
fs.writeFileSync(
|
|
393
397
|
kSystemCronPath,
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
const path = require("path");
|
|
2
2
|
const { readOpenclawConfig, writeOpenclawConfig } = require("./openclaw-config");
|
|
3
|
+
const {
|
|
4
|
+
migrateLegacyTelegramStreamingConfig,
|
|
5
|
+
} = require("./openclaw-config-migrations");
|
|
3
6
|
|
|
4
7
|
const kUsageTrackerPluginPath = path.resolve(
|
|
5
8
|
__dirname,
|
|
@@ -123,7 +126,8 @@ const ensureUsageTrackerPluginConfig = ({ fsModule, openclawDir }) => {
|
|
|
123
126
|
openclawDir,
|
|
124
127
|
fallback: {},
|
|
125
128
|
});
|
|
126
|
-
const
|
|
129
|
+
const migrated = migrateLegacyTelegramStreamingConfig(cfg);
|
|
130
|
+
const changed = reconcileManagedPluginConfig(cfg) || migrated;
|
|
127
131
|
if (!changed) return false;
|
|
128
132
|
writeOpenclawConfig({
|
|
129
133
|
fsModule,
|
|
@@ -12,6 +12,23 @@ if [[ -f "$REPO/.env" ]]; then
|
|
|
12
12
|
set +a
|
|
13
13
|
fi
|
|
14
14
|
|
|
15
|
+
if [[ -f "$REPO/cron/system-sync.json" ]]; then
|
|
16
|
+
if node - "$REPO/cron/system-sync.json" <<'NODE'
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const file = process.argv[2];
|
|
19
|
+
try {
|
|
20
|
+
const config = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
21
|
+
process.exit(config && config.enabled === false ? 0 : 1);
|
|
22
|
+
} catch {
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
NODE
|
|
26
|
+
then
|
|
27
|
+
echo "hourly-git-sync: disabled by cron/system-sync.json"
|
|
28
|
+
exit 0
|
|
29
|
+
fi
|
|
30
|
+
fi
|
|
31
|
+
|
|
15
32
|
# Drop cron scheduler runtime-only churn when it is metadata/timestamp-only.
|
|
16
33
|
maybe_restore_if_runtime_only() {
|
|
17
34
|
local file="$1"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrysb/alphaclaw",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.19",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"express": "^4.21.0",
|
|
35
35
|
"http-proxy": "^1.18.1",
|
|
36
|
-
"openclaw": "2026.
|
|
36
|
+
"openclaw": "2026.6.11",
|
|
37
37
|
"ws": "^8.19.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
@@ -51,6 +51,6 @@
|
|
|
51
51
|
"wouter-preact": "^3.7.1"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
|
-
"node": ">=22.
|
|
54
|
+
"node": ">=22.19.0"
|
|
55
55
|
}
|
|
56
56
|
}
|