@chrysb/alphaclaw 0.5.5 → 0.5.7-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/alphaclaw.js +6 -1
- package/lib/public/css/agents.css +92 -0
- package/lib/public/css/explorer.css +101 -0
- package/lib/public/css/shell.css +15 -4
- package/lib/public/js/app.js +69 -3
- package/lib/public/js/components/action-button.js +5 -0
- package/lib/public/js/components/agents-tab/agent-bindings-section/helpers.js +76 -0
- package/lib/public/js/components/agents-tab/agent-bindings-section/index.js +490 -0
- package/lib/public/js/components/agents-tab/agent-bindings-section/use-agent-bindings.js +256 -0
- package/lib/public/js/components/agents-tab/agent-detail-panel.js +74 -0
- package/lib/public/js/components/agents-tab/agent-identity-section.js +175 -0
- package/lib/public/js/components/agents-tab/agent-overview/index.js +53 -0
- package/lib/public/js/components/agents-tab/agent-overview/manage-card.js +44 -0
- package/lib/public/js/components/agents-tab/agent-overview/model-card.js +158 -0
- package/lib/public/js/components/agents-tab/agent-overview/use-model-card.js +169 -0
- package/lib/public/js/components/agents-tab/agent-overview/use-workspace-card.js +45 -0
- package/lib/public/js/components/agents-tab/agent-overview/workspace-card.js +47 -0
- package/lib/public/js/components/agents-tab/agent-pairing-section.js +265 -0
- package/lib/public/js/components/agents-tab/create-agent-modal.js +189 -0
- package/lib/public/js/components/agents-tab/create-channel-modal.js +323 -0
- package/lib/public/js/components/agents-tab/delete-agent-dialog.js +50 -0
- package/lib/public/js/components/agents-tab/edit-agent-modal.js +109 -0
- package/lib/public/js/components/agents-tab/index.js +148 -0
- package/lib/public/js/components/agents-tab/use-agents.js +89 -0
- package/lib/public/js/components/channel-account-status-badge.js +35 -0
- package/lib/public/js/components/channel-operations-panel.js +33 -0
- package/lib/public/js/components/channels.js +545 -60
- package/lib/public/js/components/envars.js +25 -4
- package/lib/public/js/components/general/index.js +21 -11
- package/lib/public/js/components/general/use-general-tab.js +78 -16
- package/lib/public/js/components/google/gmail-setup-wizard.js +1 -3
- package/lib/public/js/components/google/index.js +28 -30
- package/lib/public/js/components/icons.js +37 -0
- package/lib/public/js/components/models-tab/index.js +58 -224
- package/lib/public/js/components/models-tab/model-picker.js +212 -0
- package/lib/public/js/components/models-tab/use-models.js +17 -14
- package/lib/public/js/components/onboarding/use-welcome-pairing.js +4 -4
- package/lib/public/js/components/onboarding/welcome-pairing-step.js +2 -2
- package/lib/public/js/components/overflow-menu.js +122 -0
- package/lib/public/js/components/pairings.js +36 -8
- package/lib/public/js/components/routes/agents-route.js +27 -0
- package/lib/public/js/components/routes/general-route.js +2 -0
- package/lib/public/js/components/routes/index.js +1 -0
- package/lib/public/js/components/routes/telegram-route.js +2 -2
- package/lib/public/js/components/secret-input.js +8 -1
- package/lib/public/js/components/sidebar.js +65 -39
- package/lib/public/js/components/telegram-workspace/index.js +175 -74
- package/lib/public/js/components/telegram-workspace/manage.js +83 -10
- package/lib/public/js/components/telegram-workspace/onboarding.js +9 -8
- package/lib/public/js/components/webhooks.js +43 -18
- package/lib/public/js/hooks/use-app-shell-controller.js +7 -0
- package/lib/public/js/hooks/use-browse-navigation.js +8 -5
- package/lib/public/js/hooks/use-destination-session-selection.js +8 -1
- package/lib/public/js/lib/api.js +163 -9
- package/lib/public/js/lib/app-navigation.js +2 -1
- package/lib/public/js/lib/channel-create-operation.js +102 -0
- package/lib/public/js/lib/format.js +14 -0
- package/lib/public/js/lib/sse.js +51 -0
- package/lib/public/js/lib/telegram-api.js +38 -18
- package/lib/public/setup.html +1 -0
- package/lib/public/shared/browse-file-policies.json +0 -1
- package/lib/server/agents/service.js +1478 -0
- package/lib/server/constants.js +2 -2
- package/lib/server/env.js +3 -1
- package/lib/server/gateway.js +104 -20
- package/lib/server/gmail-serve.js +2 -12
- package/lib/server/gmail-watch.js +29 -2
- package/lib/server/onboarding/import/import-applier.js +0 -1
- package/lib/server/onboarding/index.js +0 -6
- package/lib/server/onboarding/workspace.js +74 -38
- package/lib/server/openclaw-config.js +23 -0
- package/lib/server/operation-events.js +141 -0
- package/lib/server/routes/agents.js +266 -0
- package/lib/server/routes/pairings.js +135 -25
- package/lib/server/routes/system.js +90 -10
- package/lib/server/routes/telegram.js +247 -51
- package/lib/server/startup.js +23 -0
- package/lib/server/telegram-workspace.js +61 -10
- package/lib/server/topic-registry.js +66 -7
- package/lib/server/watchdog.js +151 -27
- package/lib/server/webhooks.js +60 -12
- package/lib/server.js +40 -27
- package/lib/setup/core-prompts/AGENTS.md +6 -5
- package/lib/setup/core-prompts/TOOLS.md +1 -8
- package/package.json +1 -1
- package/lib/setup/skills/control-ui/SKILL.md +0 -62
package/lib/server/constants.js
CHANGED
|
@@ -20,7 +20,6 @@ const GATEWAY_TOKEN = process.env.OPENCLAW_GATEWAY_TOKEN || "";
|
|
|
20
20
|
const ENV_FILE_PATH = path.join(kRootDir, ".env");
|
|
21
21
|
const WORKSPACE_DIR = path.join(OPENCLAW_DIR, "workspace");
|
|
22
22
|
const kOnboardingMarkerPath = path.join(ALPHACLAW_DIR, "onboarded.json");
|
|
23
|
-
const kControlUiSkillPath = path.join(OPENCLAW_DIR, "skills", "control-ui", "SKILL.md");
|
|
24
23
|
const AUTH_PROFILES_PATH = path.join(
|
|
25
24
|
OPENCLAW_DIR,
|
|
26
25
|
"agents",
|
|
@@ -364,6 +363,8 @@ const SETUP_API_PREFIXES = [
|
|
|
364
363
|
"/api/gmail",
|
|
365
364
|
"/api/watchdog",
|
|
366
365
|
"/api/usage",
|
|
366
|
+
"/api/agents",
|
|
367
|
+
"/api/channels",
|
|
367
368
|
];
|
|
368
369
|
|
|
369
370
|
module.exports = {
|
|
@@ -381,7 +382,6 @@ module.exports = {
|
|
|
381
382
|
ENV_FILE_PATH,
|
|
382
383
|
WORKSPACE_DIR,
|
|
383
384
|
kOnboardingMarkerPath,
|
|
384
|
-
kControlUiSkillPath,
|
|
385
385
|
AUTH_PROFILES_PATH,
|
|
386
386
|
CODEX_PROFILE_ID,
|
|
387
387
|
CODEX_OAUTH_CLIENT_ID,
|
package/lib/server/env.js
CHANGED
|
@@ -64,7 +64,9 @@ const reloadEnv = () => {
|
|
|
64
64
|
const startEnvWatcher = () => {
|
|
65
65
|
try {
|
|
66
66
|
fs.watchFile(ENV_FILE_PATH, { interval: 2000 }, () => {
|
|
67
|
-
console.log(
|
|
67
|
+
console.log(
|
|
68
|
+
`[alphaclaw] ${ENV_FILE_PATH} changed externally, reloading...`,
|
|
69
|
+
);
|
|
68
70
|
reloadEnv();
|
|
69
71
|
});
|
|
70
72
|
} catch {}
|
package/lib/server/gateway.js
CHANGED
|
@@ -7,7 +7,6 @@ const {
|
|
|
7
7
|
OPENCLAW_DIR,
|
|
8
8
|
GATEWAY_HOST,
|
|
9
9
|
kDefaultGatewayPort,
|
|
10
|
-
kControlUiSkillPath,
|
|
11
10
|
kChannelDefs,
|
|
12
11
|
kOnboardingMarkerPath,
|
|
13
12
|
kRootDir,
|
|
@@ -65,9 +64,13 @@ const writeOnboardingMarker = (reason) => {
|
|
|
65
64
|
);
|
|
66
65
|
};
|
|
67
66
|
|
|
67
|
+
// Legacy backfill: older deployments may only have the control-ui skill as
|
|
68
|
+
// proof of onboarding (before the dedicated marker file existed).
|
|
69
|
+
const kLegacyControlUiSkillPath = path.join(OPENCLAW_DIR, "skills", "control-ui", "SKILL.md");
|
|
70
|
+
|
|
68
71
|
const isOnboarded = () => {
|
|
69
72
|
if (fs.existsSync(kOnboardingMarkerPath)) return true;
|
|
70
|
-
if (fs.existsSync(
|
|
73
|
+
if (fs.existsSync(kLegacyControlUiSkillPath)) {
|
|
71
74
|
writeOnboardingMarker("legacy_artifact_backfill");
|
|
72
75
|
return true;
|
|
73
76
|
}
|
|
@@ -88,6 +91,18 @@ const getGatewayPort = () => {
|
|
|
88
91
|
|
|
89
92
|
const getGatewayUrl = () => `http://${GATEWAY_HOST}:${getGatewayPort()}`;
|
|
90
93
|
|
|
94
|
+
const normalizeChannelAccountId = (value) => String(value || "").trim() || "default";
|
|
95
|
+
|
|
96
|
+
const resolveCredentialPairingAccountId = ({ channel, fileName }) => {
|
|
97
|
+
const prefix = `${String(channel || "").trim()}-`;
|
|
98
|
+
const suffix = "-allowFrom.json";
|
|
99
|
+
if (!String(fileName || "").startsWith(prefix) || !String(fileName || "").endsWith(suffix)) {
|
|
100
|
+
return "";
|
|
101
|
+
}
|
|
102
|
+
const rawAccountId = String(fileName || "").slice(prefix.length, -suffix.length);
|
|
103
|
+
return normalizeChannelAccountId(rawAccountId);
|
|
104
|
+
};
|
|
105
|
+
|
|
91
106
|
const isGatewayRunning = () =>
|
|
92
107
|
new Promise((resolve) => {
|
|
93
108
|
const sock = net.createConnection(getGatewayPort(), GATEWAY_HOST);
|
|
@@ -136,17 +151,26 @@ const launchGatewayProcess = () => {
|
|
|
136
151
|
stdio: ["pipe", "pipe", "pipe"],
|
|
137
152
|
});
|
|
138
153
|
gatewayChild = child;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
154
|
+
let didSignalGatewayReady = false;
|
|
155
|
+
child.stdout.on("data", (d) => {
|
|
156
|
+
const text = Buffer.isBuffer(d) ? d.toString("utf8") : String(d ?? "");
|
|
157
|
+
if (
|
|
158
|
+
!didSignalGatewayReady &&
|
|
159
|
+
gatewayLaunchHandler &&
|
|
160
|
+
text.toLowerCase().includes("listening on")
|
|
161
|
+
) {
|
|
162
|
+
didSignalGatewayReady = true;
|
|
163
|
+
try {
|
|
164
|
+
gatewayLaunchHandler({
|
|
165
|
+
pid: child.pid,
|
|
166
|
+
startedAt: Date.now(),
|
|
167
|
+
});
|
|
168
|
+
} catch (err) {
|
|
169
|
+
console.error(`[alphaclaw] Gateway launch handler error: ${err.message}`);
|
|
170
|
+
}
|
|
147
171
|
}
|
|
148
|
-
|
|
149
|
-
|
|
172
|
+
process.stdout.write(`[gateway] ${d}`);
|
|
173
|
+
});
|
|
150
174
|
child.stderr.on("data", (d) => {
|
|
151
175
|
appendStderrTail(d);
|
|
152
176
|
process.stderr.write(`[gateway] ${d}`);
|
|
@@ -206,6 +230,12 @@ const restartGateway = (reloadEnv) => {
|
|
|
206
230
|
runGatewayCmd("--force");
|
|
207
231
|
};
|
|
208
232
|
|
|
233
|
+
const restartGatewayLight = (reloadEnv) => {
|
|
234
|
+
reloadEnv();
|
|
235
|
+
markManagedGatewayExitExpected();
|
|
236
|
+
runGatewayCmd("restart");
|
|
237
|
+
};
|
|
238
|
+
|
|
209
239
|
const attachGatewaySignalHandlers = () => {
|
|
210
240
|
process.on("SIGTERM", () => {
|
|
211
241
|
runGatewayCmd("stop");
|
|
@@ -324,10 +354,36 @@ const getChannelStatus = () => {
|
|
|
324
354
|
const channels = {};
|
|
325
355
|
|
|
326
356
|
for (const ch of ["telegram", "discord"]) {
|
|
327
|
-
|
|
328
|
-
|
|
357
|
+
const channelConfig =
|
|
358
|
+
config.channels?.[ch] && typeof config.channels[ch] === "object"
|
|
359
|
+
? config.channels[ch]
|
|
360
|
+
: null;
|
|
361
|
+
if (!channelConfig?.enabled) continue;
|
|
329
362
|
|
|
330
|
-
|
|
363
|
+
const rawAccounts =
|
|
364
|
+
channelConfig.accounts && typeof channelConfig.accounts === "object"
|
|
365
|
+
? channelConfig.accounts
|
|
366
|
+
: {};
|
|
367
|
+
const accountEntries = Object.keys(rawAccounts).length > 0
|
|
368
|
+
? Object.entries(rawAccounts)
|
|
369
|
+
: [["default", channelConfig]];
|
|
370
|
+
const configuredAccountIds = new Set(
|
|
371
|
+
accountEntries.map(([accountId]) => normalizeChannelAccountId(accountId)),
|
|
372
|
+
);
|
|
373
|
+
const hasConfiguredToken = accountEntries.some(([accountId, accountConfig]) => {
|
|
374
|
+
const normalizedAccountId = normalizeChannelAccountId(accountId);
|
|
375
|
+
const envKey = normalizedAccountId === "default"
|
|
376
|
+
? kChannelDefs[ch].envKey
|
|
377
|
+
: `${kChannelDefs[ch].envKey}_${normalizedAccountId.replace(/-/g, "_").toUpperCase()}`;
|
|
378
|
+
return !!process.env[envKey]
|
|
379
|
+
|| !!accountConfig?.botToken
|
|
380
|
+
|| !!accountConfig?.token;
|
|
381
|
+
});
|
|
382
|
+
if (!hasConfiguredToken) continue;
|
|
383
|
+
|
|
384
|
+
const pairedByAccount = new Map(
|
|
385
|
+
Array.from(configuredAccountIds).map((accountId) => [accountId, 0]),
|
|
386
|
+
);
|
|
331
387
|
try {
|
|
332
388
|
const files = fs
|
|
333
389
|
.readdirSync(credDir)
|
|
@@ -335,16 +391,43 @@ const getChannelStatus = () => {
|
|
|
335
391
|
(f) => f.startsWith(`${ch}-`) && f.endsWith("-allowFrom.json"),
|
|
336
392
|
);
|
|
337
393
|
for (const file of files) {
|
|
394
|
+
const accountId = resolveCredentialPairingAccountId({
|
|
395
|
+
channel: ch,
|
|
396
|
+
fileName: file,
|
|
397
|
+
});
|
|
398
|
+
if (!accountId || !configuredAccountIds.has(accountId)) continue;
|
|
338
399
|
const data = JSON.parse(
|
|
339
400
|
fs.readFileSync(`${credDir}/${file}`, "utf8"),
|
|
340
401
|
);
|
|
341
|
-
|
|
402
|
+
const nextCount =
|
|
403
|
+
Number(pairedByAccount.get(accountId) || 0)
|
|
404
|
+
+ (Array.isArray(data.allowFrom) ? data.allowFrom.length : 0);
|
|
405
|
+
pairedByAccount.set(accountId, nextCount);
|
|
342
406
|
}
|
|
343
407
|
} catch {}
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
408
|
+
for (const [accountId, accountConfig] of accountEntries) {
|
|
409
|
+
const inlineAllowFrom = accountConfig?.allowFrom;
|
|
410
|
+
if (!Array.isArray(inlineAllowFrom)) continue;
|
|
411
|
+
const normalizedAccountId = normalizeChannelAccountId(accountId);
|
|
412
|
+
const nextCount =
|
|
413
|
+
Number(pairedByAccount.get(normalizedAccountId) || 0) + inlineAllowFrom.length;
|
|
414
|
+
pairedByAccount.set(normalizedAccountId, nextCount);
|
|
415
|
+
}
|
|
416
|
+
const accounts = Object.fromEntries(
|
|
417
|
+
Array.from(pairedByAccount.entries()).map(([accountId, paired]) => [
|
|
418
|
+
accountId,
|
|
419
|
+
{ status: paired > 0 ? "paired" : "configured", paired },
|
|
420
|
+
]),
|
|
421
|
+
);
|
|
422
|
+
const paired = Array.from(pairedByAccount.values()).reduce(
|
|
423
|
+
(total, count) => total + Number(count || 0),
|
|
424
|
+
0,
|
|
425
|
+
);
|
|
426
|
+
channels[ch] = {
|
|
427
|
+
status: paired > 0 ? "paired" : "configured",
|
|
428
|
+
paired,
|
|
429
|
+
accounts,
|
|
430
|
+
};
|
|
348
431
|
}
|
|
349
432
|
|
|
350
433
|
return channels;
|
|
@@ -365,6 +448,7 @@ module.exports = {
|
|
|
365
448
|
runGatewayCmd,
|
|
366
449
|
startGateway,
|
|
367
450
|
restartGateway,
|
|
451
|
+
restartGatewayLight,
|
|
368
452
|
attachGatewaySignalHandlers,
|
|
369
453
|
ensureGatewayProxyConfig,
|
|
370
454
|
syncChannelConfig,
|
|
@@ -124,18 +124,8 @@ const createGmailServeManager = ({
|
|
|
124
124
|
stdio: ["ignore", "pipe", "pipe"],
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
-
child.stdout.on("data", (
|
|
128
|
-
|
|
129
|
-
if (line) {
|
|
130
|
-
console.log(`[alphaclaw] gmail watch serve (${email}): ${line}`);
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
child.stderr.on("data", (chunk) => {
|
|
134
|
-
const line = String(chunk || "").trim();
|
|
135
|
-
if (line) {
|
|
136
|
-
console.log(`[alphaclaw] gmail watch serve stderr (${email}): ${line}`);
|
|
137
|
-
}
|
|
138
|
-
});
|
|
127
|
+
child.stdout.on("data", () => {});
|
|
128
|
+
child.stderr.on("data", () => {});
|
|
139
129
|
|
|
140
130
|
const nextEntry = {
|
|
141
131
|
accountId,
|
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
const { createGmailServeManager } = require("./gmail-serve");
|
|
17
17
|
const { parseJsonObjectFromNoisyOutput, parseJsonSafe } = require("./utils/json");
|
|
18
18
|
const { createWebhook } = require("./webhooks");
|
|
19
|
+
const { readOpenclawConfig } = require("./openclaw-config");
|
|
19
20
|
const { quoteShellArg } = require("./utils/shell");
|
|
20
21
|
|
|
21
22
|
const parseExpirationFromOutput = (raw) => {
|
|
@@ -65,11 +66,16 @@ const normalizeDestination = (destination = null) => {
|
|
|
65
66
|
if (!destination || typeof destination !== "object") return null;
|
|
66
67
|
const channel = String(destination?.channel || "").trim();
|
|
67
68
|
const to = String(destination?.to || "").trim();
|
|
68
|
-
|
|
69
|
+
const agentId = String(destination?.agentId || "").trim();
|
|
70
|
+
if (!channel && !to && !agentId) return null;
|
|
69
71
|
if (!channel || !to) {
|
|
70
72
|
throw new Error("destination.channel and destination.to are required");
|
|
71
73
|
}
|
|
72
|
-
return {
|
|
74
|
+
return {
|
|
75
|
+
channel,
|
|
76
|
+
to,
|
|
77
|
+
...(agentId ? { agentId } : {}),
|
|
78
|
+
};
|
|
73
79
|
};
|
|
74
80
|
|
|
75
81
|
const buildGmailTransformSource = (destination = null) => {
|
|
@@ -91,6 +97,9 @@ const buildGmailTransformSource = (destination = null) => {
|
|
|
91
97
|
? [
|
|
92
98
|
` channel: ${JSON.stringify(normalizedDestination.channel)},`,
|
|
93
99
|
` to: ${JSON.stringify(normalizedDestination.to)},`,
|
|
100
|
+
...(normalizedDestination.agentId
|
|
101
|
+
? [` agentId: ${JSON.stringify(normalizedDestination.agentId)},`]
|
|
102
|
+
: []),
|
|
94
103
|
]
|
|
95
104
|
: []),
|
|
96
105
|
" };",
|
|
@@ -99,6 +108,18 @@ const buildGmailTransformSource = (destination = null) => {
|
|
|
99
108
|
].join("\n");
|
|
100
109
|
};
|
|
101
110
|
|
|
111
|
+
const hasGmailWebhookMapping = ({ fs, openclawDir }) => {
|
|
112
|
+
const cfg = readOpenclawConfig({
|
|
113
|
+
fsModule: fs,
|
|
114
|
+
openclawDir,
|
|
115
|
+
fallback: {},
|
|
116
|
+
});
|
|
117
|
+
const mappings = Array.isArray(cfg?.hooks?.mappings) ? cfg.hooks.mappings : [];
|
|
118
|
+
return mappings.some(
|
|
119
|
+
(mapping) => String(mapping?.match?.path || "").trim().toLowerCase() === "gmail",
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
102
123
|
const getGmailTransformAbsolutePath = (constants) =>
|
|
103
124
|
path.join(constants.OPENCLAW_DIR, "hooks/transforms/gmail/gmail-transform.mjs");
|
|
104
125
|
|
|
@@ -295,6 +316,7 @@ const createGmailWatchService = ({
|
|
|
295
316
|
transform: { module: gmailTransformModulePath },
|
|
296
317
|
},
|
|
297
318
|
transformSource: buildGmailTransformSource(destination),
|
|
319
|
+
overwriteTransform: true,
|
|
298
320
|
});
|
|
299
321
|
const webhookAfter = fs.readFileSync(configPath, "utf8");
|
|
300
322
|
if (webhookBefore !== webhookAfter) {
|
|
@@ -367,6 +389,10 @@ const createGmailWatchService = ({
|
|
|
367
389
|
String(push.token || ""),
|
|
368
390
|
)}`;
|
|
369
391
|
const transformExists = fs.existsSync(getGmailTransformAbsolutePath(constants));
|
|
392
|
+
const webhookExists = hasGmailWebhookMapping({
|
|
393
|
+
fs,
|
|
394
|
+
openclawDir: constants.OPENCLAW_DIR,
|
|
395
|
+
});
|
|
370
396
|
const commands =
|
|
371
397
|
projectId && push.token
|
|
372
398
|
? {
|
|
@@ -385,6 +411,7 @@ const createGmailWatchService = ({
|
|
|
385
411
|
pushEndpoint,
|
|
386
412
|
commands,
|
|
387
413
|
transformExists,
|
|
414
|
+
webhookExists,
|
|
388
415
|
configured: Boolean(topicPath && push.token && projectId),
|
|
389
416
|
};
|
|
390
417
|
};
|
|
@@ -17,7 +17,6 @@ const {
|
|
|
17
17
|
} = require("./openclaw");
|
|
18
18
|
const {
|
|
19
19
|
ensureOpenclawRuntimeArtifacts,
|
|
20
|
-
installControlUiSkill,
|
|
21
20
|
syncBootstrapPromptFiles,
|
|
22
21
|
} = require("./workspace");
|
|
23
22
|
const {
|
|
@@ -498,11 +497,6 @@ const createOnboardingService = ({
|
|
|
498
497
|
authProfiles?.syncConfigAuthReferencesForAgent?.();
|
|
499
498
|
ensureGatewayProxyConfig(getBaseUrl(req));
|
|
500
499
|
|
|
501
|
-
installControlUiSkill({
|
|
502
|
-
fs,
|
|
503
|
-
openclawDir: OPENCLAW_DIR,
|
|
504
|
-
baseUrl: getBaseUrl(req),
|
|
505
|
-
});
|
|
506
500
|
installGogCliSkill({ fs, openclawDir: OPENCLAW_DIR });
|
|
507
501
|
|
|
508
502
|
installHourlyGitSyncScript({ fs, openclawDir: OPENCLAW_DIR });
|
|
@@ -32,7 +32,17 @@ const isTelegramWorkspaceEnabled = (fs) => {
|
|
|
32
32
|
try {
|
|
33
33
|
const configPath = `${OPENCLAW_DIR}/openclaw.json`;
|
|
34
34
|
const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
35
|
-
|
|
35
|
+
const telegramConfig = cfg.channels?.telegram || {};
|
|
36
|
+
const topLevelGroupCount = Object.keys(telegramConfig.groups || {}).length;
|
|
37
|
+
if (topLevelGroupCount > 0) return true;
|
|
38
|
+
const accounts =
|
|
39
|
+
telegramConfig.accounts && typeof telegramConfig.accounts === "object"
|
|
40
|
+
? telegramConfig.accounts
|
|
41
|
+
: {};
|
|
42
|
+
for (const accountConfig of Object.values(accounts)) {
|
|
43
|
+
if (Object.keys(accountConfig?.groups || {}).length > 0) return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
36
46
|
} catch {
|
|
37
47
|
return false;
|
|
38
48
|
}
|
|
@@ -73,59 +83,85 @@ const renderGoogleAccountsMarkdown = (fs) => {
|
|
|
73
83
|
}
|
|
74
84
|
};
|
|
75
85
|
|
|
86
|
+
const resolveAllAgentWorkspaces = (fs) => {
|
|
87
|
+
try {
|
|
88
|
+
const configPath = path.join(OPENCLAW_DIR, "openclaw.json");
|
|
89
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
90
|
+
const list = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
|
|
91
|
+
return list
|
|
92
|
+
.map((entry) => {
|
|
93
|
+
const agentId = String(entry.id || "").trim();
|
|
94
|
+
const workspace = String(entry.workspace || "").trim();
|
|
95
|
+
if (!agentId || !workspace) return null;
|
|
96
|
+
return {
|
|
97
|
+
agentId,
|
|
98
|
+
workspace,
|
|
99
|
+
};
|
|
100
|
+
})
|
|
101
|
+
.filter(Boolean);
|
|
102
|
+
} catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
76
107
|
const syncBootstrapPromptFiles = ({ fs, workspaceDir, baseUrl }) => {
|
|
77
108
|
try {
|
|
78
109
|
const setupUiUrl = resolveSetupUiUrl(baseUrl);
|
|
79
|
-
const bootstrapDir = path.join(workspaceDir, "hooks", "bootstrap");
|
|
80
|
-
fs.mkdirSync(bootstrapDir, { recursive: true });
|
|
81
|
-
|
|
82
|
-
// AlphaClaw-managed files are always overwritten (even during import)
|
|
83
|
-
fs.copyFileSync(
|
|
84
|
-
path.join(kSetupDir, "core-prompts", "AGENTS.md"),
|
|
85
|
-
path.join(bootstrapDir, "AGENTS.md"),
|
|
86
|
-
);
|
|
87
110
|
|
|
88
111
|
const toolsTemplate = fs.readFileSync(
|
|
89
112
|
path.join(kSetupDir, "core-prompts", "TOOLS.md"),
|
|
90
113
|
"utf8",
|
|
91
114
|
);
|
|
92
|
-
|
|
93
|
-
/\{\{SETUP_UI_URL\}\}/g,
|
|
94
|
-
setupUiUrl,
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
const topicSection = renderTopicRegistryMarkdown({
|
|
98
|
-
includeSyncGuidance: isTelegramWorkspaceEnabled(fs),
|
|
99
|
-
});
|
|
100
|
-
if (topicSection) {
|
|
101
|
-
toolsContent += topicSection;
|
|
102
|
-
}
|
|
115
|
+
const includeSyncGuidance = isTelegramWorkspaceEnabled(fs);
|
|
103
116
|
const googleAccountsSection = renderGoogleAccountsMarkdown(fs);
|
|
104
|
-
|
|
105
|
-
toolsContent
|
|
117
|
+
const buildToolsContent = ({ agentId = "" } = {}) => {
|
|
118
|
+
let toolsContent = toolsTemplate.replace(/\{\{SETUP_UI_URL\}\}/g, setupUiUrl);
|
|
119
|
+
const topicSection = renderTopicRegistryMarkdown({
|
|
120
|
+
includeSyncGuidance,
|
|
121
|
+
agentId,
|
|
122
|
+
});
|
|
123
|
+
if (topicSection) {
|
|
124
|
+
toolsContent += topicSection;
|
|
125
|
+
}
|
|
126
|
+
if (googleAccountsSection) {
|
|
127
|
+
toolsContent += googleAccountsSection;
|
|
128
|
+
}
|
|
129
|
+
return toolsContent;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const agentsSourcePath = path.join(kSetupDir, "core-prompts", "AGENTS.md");
|
|
133
|
+
|
|
134
|
+
const writeToWorkspace = (targetDir, toolsContent) => {
|
|
135
|
+
const bootstrapDir = path.join(targetDir, "hooks", "bootstrap");
|
|
136
|
+
fs.mkdirSync(bootstrapDir, { recursive: true });
|
|
137
|
+
fs.copyFileSync(agentsSourcePath, path.join(bootstrapDir, "AGENTS.md"));
|
|
138
|
+
fs.writeFileSync(path.join(bootstrapDir, "TOOLS.md"), toolsContent);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
writeToWorkspace(workspaceDir, buildToolsContent());
|
|
142
|
+
|
|
143
|
+
const otherWorkspaces = resolveAllAgentWorkspaces(fs).filter(
|
|
144
|
+
(entry) => path.resolve(entry.workspace) !== path.resolve(workspaceDir),
|
|
145
|
+
);
|
|
146
|
+
for (const entry of otherWorkspaces) {
|
|
147
|
+
try {
|
|
148
|
+
writeToWorkspace(
|
|
149
|
+
entry.workspace,
|
|
150
|
+
buildToolsContent({ agentId: entry.agentId }),
|
|
151
|
+
);
|
|
152
|
+
} catch (e) {
|
|
153
|
+
console.error(
|
|
154
|
+
`[onboard] Bootstrap sync skipped for ${entry.workspace}: ${e.message}`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
106
157
|
}
|
|
107
158
|
|
|
108
|
-
fs.writeFileSync(path.join(bootstrapDir, "TOOLS.md"), toolsContent);
|
|
109
159
|
console.log("[onboard] Bootstrap prompt files synced");
|
|
110
160
|
} catch (e) {
|
|
111
161
|
console.error("[onboard] Bootstrap prompt sync error:", e.message);
|
|
112
162
|
}
|
|
113
163
|
};
|
|
114
164
|
|
|
115
|
-
const installControlUiSkill = ({ fs, openclawDir, baseUrl }) => {
|
|
116
|
-
try {
|
|
117
|
-
const setupUiUrl = resolveSetupUiUrl(baseUrl);
|
|
118
|
-
const skillDir = `${openclawDir}/skills/control-ui`;
|
|
119
|
-
fs.mkdirSync(skillDir, { recursive: true });
|
|
120
|
-
const skillTemplate = fs.readFileSync(path.join(kSetupDir, "skills", "control-ui", "SKILL.md"), "utf8");
|
|
121
|
-
const skillContent = skillTemplate.replace(/\{\{BASE_URL\}\}/g, setupUiUrl);
|
|
122
|
-
fs.writeFileSync(`${skillDir}/SKILL.md`, skillContent);
|
|
123
|
-
console.log(`[onboard] Control UI skill installed (${setupUiUrl})`);
|
|
124
|
-
} catch (e) {
|
|
125
|
-
console.error("[onboard] Skill install error:", e.message);
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
|
|
129
165
|
const ensureOpenclawRuntimeArtifacts = ({
|
|
130
166
|
fs,
|
|
131
167
|
openclawDir,
|
|
@@ -153,6 +189,6 @@ const ensureOpenclawRuntimeArtifacts = ({
|
|
|
153
189
|
|
|
154
190
|
module.exports = {
|
|
155
191
|
ensureOpenclawRuntimeArtifacts,
|
|
156
|
-
|
|
192
|
+
resolveSetupUiUrl,
|
|
157
193
|
syncBootstrapPromptFiles,
|
|
158
194
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const resolveOpenclawConfigPath = ({ openclawDir }) =>
|
|
5
|
+
path.join(openclawDir, "openclaw.json");
|
|
6
|
+
|
|
7
|
+
const readOpenclawConfig = ({
|
|
8
|
+
fsModule = fs,
|
|
9
|
+
openclawDir,
|
|
10
|
+
fallback = {},
|
|
11
|
+
} = {}) => {
|
|
12
|
+
const configPath = resolveOpenclawConfigPath({ openclawDir });
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(fsModule.readFileSync(configPath, "utf8"));
|
|
15
|
+
} catch {
|
|
16
|
+
return fallback;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
resolveOpenclawConfigPath,
|
|
22
|
+
readOpenclawConfig,
|
|
23
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
|
|
3
|
+
const kDefaultTtlMs = 5 * 60 * 1000;
|
|
4
|
+
const kMaxEventsPerOperation = 200;
|
|
5
|
+
|
|
6
|
+
const formatSseEvent = ({ id, event, data }) => {
|
|
7
|
+
const lines = [];
|
|
8
|
+
if (id) lines.push(`id: ${id}`);
|
|
9
|
+
if (event) lines.push(`event: ${event}`);
|
|
10
|
+
const payload = JSON.stringify(data === undefined ? {} : data);
|
|
11
|
+
for (const line of payload.split("\n")) {
|
|
12
|
+
lines.push(`data: ${line}`);
|
|
13
|
+
}
|
|
14
|
+
return `${lines.join("\n")}\n\n`;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const createOperationEventsService = ({ ttlMs = kDefaultTtlMs } = {}) => {
|
|
18
|
+
const operations = new Map();
|
|
19
|
+
let sweepTimer = null;
|
|
20
|
+
|
|
21
|
+
const ensureSweeper = () => {
|
|
22
|
+
if (sweepTimer) return;
|
|
23
|
+
sweepTimer = setInterval(() => {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
for (const [operationId, state] of operations.entries()) {
|
|
26
|
+
if (state.expiresAt <= now && state.subscribers.size === 0) {
|
|
27
|
+
operations.delete(operationId);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}, 30_000);
|
|
31
|
+
sweepTimer.unref();
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const getOperation = (operationId) => {
|
|
35
|
+
const normalized = String(operationId || "").trim();
|
|
36
|
+
if (!normalized) return null;
|
|
37
|
+
return operations.get(normalized) || null;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const createOperation = ({ type = "operation" } = {}) => {
|
|
41
|
+
const operationId = crypto.randomUUID();
|
|
42
|
+
operations.set(operationId, {
|
|
43
|
+
id: operationId,
|
|
44
|
+
type: String(type || "operation").trim() || "operation",
|
|
45
|
+
createdAt: Date.now(),
|
|
46
|
+
expiresAt: Date.now() + ttlMs,
|
|
47
|
+
status: "pending",
|
|
48
|
+
nextEventId: 1,
|
|
49
|
+
events: [],
|
|
50
|
+
subscribers: new Set(),
|
|
51
|
+
});
|
|
52
|
+
ensureSweeper();
|
|
53
|
+
return { operationId };
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const publish = (operationId, { event = "message", data = {} } = {}) => {
|
|
57
|
+
const state = getOperation(operationId);
|
|
58
|
+
if (!state) return false;
|
|
59
|
+
const entry = {
|
|
60
|
+
id: String(state.nextEventId++),
|
|
61
|
+
event: String(event || "message").trim() || "message",
|
|
62
|
+
data: data === undefined ? {} : data,
|
|
63
|
+
ts: Date.now(),
|
|
64
|
+
};
|
|
65
|
+
state.events.push(entry);
|
|
66
|
+
if (state.events.length > kMaxEventsPerOperation) {
|
|
67
|
+
state.events = state.events.slice(-kMaxEventsPerOperation);
|
|
68
|
+
}
|
|
69
|
+
for (const res of state.subscribers) {
|
|
70
|
+
try {
|
|
71
|
+
res.write(formatSseEvent(entry));
|
|
72
|
+
} catch {}
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const complete = (operationId, payload = {}) => {
|
|
78
|
+
const state = getOperation(operationId);
|
|
79
|
+
if (!state) return false;
|
|
80
|
+
state.status = "completed";
|
|
81
|
+
state.expiresAt = Date.now() + ttlMs;
|
|
82
|
+
publish(operationId, {
|
|
83
|
+
event: "done",
|
|
84
|
+
data: payload,
|
|
85
|
+
});
|
|
86
|
+
return true;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const fail = (operationId, error) => {
|
|
90
|
+
const state = getOperation(operationId);
|
|
91
|
+
if (!state) return false;
|
|
92
|
+
state.status = "failed";
|
|
93
|
+
state.expiresAt = Date.now() + ttlMs;
|
|
94
|
+
publish(operationId, {
|
|
95
|
+
event: "error",
|
|
96
|
+
data: {
|
|
97
|
+
error: String(error?.message || error || "Operation failed"),
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
return true;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const subscribe = ({ operationId, req, res }) => {
|
|
104
|
+
const state = getOperation(operationId);
|
|
105
|
+
if (!state) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
res.status(200);
|
|
109
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
110
|
+
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
111
|
+
res.setHeader("Connection", "keep-alive");
|
|
112
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
113
|
+
res.flushHeaders?.();
|
|
114
|
+
res.write(": connected\n\n");
|
|
115
|
+
for (const event of state.events) {
|
|
116
|
+
res.write(formatSseEvent(event));
|
|
117
|
+
}
|
|
118
|
+
state.subscribers.add(res);
|
|
119
|
+
const close = () => {
|
|
120
|
+
state.subscribers.delete(res);
|
|
121
|
+
if (state.expiresAt <= Date.now() && state.subscribers.size === 0) {
|
|
122
|
+
operations.delete(state.id);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
req.on("close", close);
|
|
126
|
+
return true;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
createOperation,
|
|
131
|
+
publish,
|
|
132
|
+
complete,
|
|
133
|
+
fail,
|
|
134
|
+
subscribe,
|
|
135
|
+
getOperation,
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
module.exports = {
|
|
140
|
+
createOperationEventsService,
|
|
141
|
+
};
|