@chrysb/alphaclaw 0.6.2-beta.4 → 0.6.2-beta.6
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/lib/public/assets/icons/slack.svg +17 -0
- package/lib/public/css/cron.css +91 -39
- package/lib/public/js/components/add-channel-menu.js +59 -0
- package/lib/public/js/components/agents-tab/agent-bindings-section/index.js +14 -38
- package/lib/public/js/components/agents-tab/agent-bindings-section/use-channel-items.js +0 -6
- package/lib/public/js/components/agents-tab/create-channel-modal.js +185 -47
- package/lib/public/js/components/channels.js +15 -44
- package/lib/public/js/components/cron-tab/cron-calendar.js +287 -164
- package/lib/public/js/components/cron-tab/cron-insights-panel.js +325 -0
- package/lib/public/js/components/cron-tab/cron-job-detail.js +38 -363
- package/lib/public/js/components/cron-tab/cron-job-settings-card.js +233 -0
- package/lib/public/js/components/cron-tab/cron-overview.js +40 -19
- package/lib/public/js/components/cron-tab/cron-prompt-editor.js +173 -0
- package/lib/public/js/components/cron-tab/cron-run-history-panel.js +69 -56
- package/lib/public/js/components/cron-tab/cron-runs-trend-card.js +20 -2
- package/lib/public/js/components/cron-tab/index.js +170 -78
- package/lib/public/js/components/envars.js +4 -3
- package/lib/public/js/components/file-viewer/editor-surface.js +5 -1
- package/lib/public/js/components/file-viewer/use-editor-line-number-sync.js +36 -0
- package/lib/public/js/components/file-viewer/use-file-viewer.js +7 -23
- package/lib/public/js/components/file-viewer/utils.js +1 -5
- package/lib/public/js/components/onboarding/pairing-utils.js +1 -0
- package/lib/public/js/components/onboarding/welcome-config.js +31 -1
- package/lib/public/js/components/onboarding/welcome-form-step.js +145 -67
- package/lib/public/js/components/onboarding/welcome-pairing-step.js +89 -50
- package/lib/public/js/components/pairings.js +1 -1
- package/lib/public/js/components/welcome/index.js +1 -0
- package/lib/public/js/lib/channel-provider-availability.js +23 -0
- package/lib/server/agents/channels.js +110 -6
- package/lib/server/agents/shared.js +70 -1
- package/lib/server/constants.js +13 -0
- package/lib/server/gateway.js +28 -11
- package/lib/server/onboarding/openclaw.js +30 -0
- package/lib/server/onboarding/validation.js +1 -1
- package/lib/server/routes/pairings.js +2 -2
- package/lib/server/routes/system.js +9 -2
- package/lib/server/slack-api.js +38 -0
- package/lib/server/watchdog-notify.js +20 -3
- package/lib/server.js +3 -1
- package/package.json +1 -1
|
@@ -15,6 +15,7 @@ const {
|
|
|
15
15
|
isValidChannelAccountId,
|
|
16
16
|
normalizeChannelProvider,
|
|
17
17
|
deriveChannelEnvKey,
|
|
18
|
+
deriveChannelExtraEnvKeys,
|
|
18
19
|
getConfiguredChannelEnvKeys,
|
|
19
20
|
assertActiveChannelTokenEnvVars,
|
|
20
21
|
normalizeChannelConfig,
|
|
@@ -63,15 +64,28 @@ const createChannelsDomain = ({
|
|
|
63
64
|
throw new Error(`Channel account "${provider}/${accountId}" not found`);
|
|
64
65
|
}
|
|
65
66
|
const envKey = deriveChannelEnvKey({ provider, accountId });
|
|
67
|
+
const extraEnvKeys = deriveChannelExtraEnvKeys({ provider, accountId });
|
|
66
68
|
const envVars = readEnvFile();
|
|
67
69
|
const envEntry = (Array.isArray(envVars) ? envVars : []).find(
|
|
68
70
|
(entry) => String(entry?.key || "").trim() === envKey,
|
|
69
71
|
);
|
|
72
|
+
const appEnvKey = extraEnvKeys[0] || "";
|
|
73
|
+
const appEnvEntry = appEnvKey
|
|
74
|
+
? (Array.isArray(envVars) ? envVars : []).find(
|
|
75
|
+
(entry) => String(entry?.key || "").trim() === appEnvKey,
|
|
76
|
+
)
|
|
77
|
+
: null;
|
|
70
78
|
return {
|
|
71
79
|
provider,
|
|
72
80
|
accountId,
|
|
73
81
|
envKey,
|
|
74
82
|
token: String(envEntry?.value || ""),
|
|
83
|
+
...(provider === "slack"
|
|
84
|
+
? {
|
|
85
|
+
appEnvKey,
|
|
86
|
+
appToken: String(appEnvEntry?.value || ""),
|
|
87
|
+
}
|
|
88
|
+
: {}),
|
|
75
89
|
};
|
|
76
90
|
};
|
|
77
91
|
|
|
@@ -129,11 +143,21 @@ const createChannelsDomain = ({
|
|
|
129
143
|
`Channel account "${provider}/${accountId}" already exists`,
|
|
130
144
|
);
|
|
131
145
|
}
|
|
132
|
-
if (
|
|
133
|
-
|
|
146
|
+
if (
|
|
147
|
+
(provider === "discord" || provider === "slack") &&
|
|
148
|
+
Object.keys(existingAccounts).length > 0
|
|
149
|
+
) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`${kChannelLabels[provider] || "This provider"} supports a single channel account`,
|
|
152
|
+
);
|
|
134
153
|
}
|
|
135
154
|
|
|
136
155
|
const envKey = deriveChannelEnvKey({ provider, accountId });
|
|
156
|
+
const extraEnvKeys = deriveChannelExtraEnvKeys({ provider, accountId });
|
|
157
|
+
const appToken = String(input.appToken || "").trim();
|
|
158
|
+
if (provider === "slack" && !appToken) {
|
|
159
|
+
throw new Error("Slack App Token is required");
|
|
160
|
+
}
|
|
137
161
|
const tokenField = kChannelTokenFields[provider];
|
|
138
162
|
const currentEnvVars = readEnvFile();
|
|
139
163
|
const previousEnvVars = Array.isArray(currentEnvVars) ? currentEnvVars : [];
|
|
@@ -156,11 +180,41 @@ const createChannelsDomain = ({
|
|
|
156
180
|
`[alphaclaw] Overwriting orphaned channel env var ${dupKey} (no matching config entry)`,
|
|
157
181
|
);
|
|
158
182
|
}
|
|
183
|
+
let orphanedExtraEnvKey = null;
|
|
184
|
+
if (provider === "slack") {
|
|
185
|
+
const appEnvKey = extraEnvKeys[0];
|
|
186
|
+
const duplicateAppTokenEntry = previousEnvVars.find((entry) => {
|
|
187
|
+
const existingKey = String(entry?.key || "").trim();
|
|
188
|
+
const existingValue = String(entry?.value || "").trim();
|
|
189
|
+
if (!existingKey || !existingValue) return false;
|
|
190
|
+
if (existingKey === envKey || existingKey === appEnvKey) return false;
|
|
191
|
+
return existingValue === appToken;
|
|
192
|
+
});
|
|
193
|
+
if (duplicateAppTokenEntry) {
|
|
194
|
+
const dupKey = String(duplicateAppTokenEntry.key || "").trim();
|
|
195
|
+
const configuredKeys = getConfiguredChannelEnvKeys(cfg);
|
|
196
|
+
if (configuredKeys.has(dupKey)) {
|
|
197
|
+
throw new Error(`Channel token already exists in ${dupKey}`);
|
|
198
|
+
}
|
|
199
|
+
orphanedExtraEnvKey = dupKey;
|
|
200
|
+
console.log(
|
|
201
|
+
`[alphaclaw] Overwriting orphaned channel env var ${dupKey} (no matching config entry)`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
159
205
|
const nextEnvVars = previousEnvVars.filter((entry) => {
|
|
160
206
|
const key = String(entry?.key || "").trim();
|
|
161
|
-
return
|
|
207
|
+
return (
|
|
208
|
+
key !== envKey &&
|
|
209
|
+
key !== orphanedEnvKey &&
|
|
210
|
+
!extraEnvKeys.includes(key) &&
|
|
211
|
+
key !== orphanedExtraEnvKey
|
|
212
|
+
);
|
|
162
213
|
});
|
|
163
214
|
nextEnvVars.push({ key: envKey, value: token });
|
|
215
|
+
if (provider === "slack" && extraEnvKeys[0]) {
|
|
216
|
+
nextEnvVars.push({ key: extraEnvKeys[0], value: appToken });
|
|
217
|
+
}
|
|
164
218
|
|
|
165
219
|
const previousConfig = cloneJson(cfg);
|
|
166
220
|
try {
|
|
@@ -188,7 +242,12 @@ const createChannelsDomain = ({
|
|
|
188
242
|
? `--account ${shellEscapeArg(accountId)}`
|
|
189
243
|
: "",
|
|
190
244
|
name ? `--name ${shellEscapeArg(name)}` : "",
|
|
191
|
-
|
|
245
|
+
provider === "slack"
|
|
246
|
+
? `--bot-token ${shellEscapeArg(token)}`
|
|
247
|
+
: `--token ${shellEscapeArg(token)}`,
|
|
248
|
+
provider === "slack" && appToken
|
|
249
|
+
? `--app-token ${shellEscapeArg(appToken)}`
|
|
250
|
+
: "",
|
|
192
251
|
].filter(Boolean);
|
|
193
252
|
const addResult = await clawCmd(addArgs.join(" "), {
|
|
194
253
|
quiet: true,
|
|
@@ -225,6 +284,9 @@ const createChannelsDomain = ({
|
|
|
225
284
|
: {}),
|
|
226
285
|
...(name ? { name } : {}),
|
|
227
286
|
[tokenField]: `\${${envKey}}`,
|
|
287
|
+
...(provider === "slack" && extraEnvKeys[0]
|
|
288
|
+
? { appToken: `\${${extraEnvKeys[0]}}` }
|
|
289
|
+
: {}),
|
|
228
290
|
dmPolicy: "pairing",
|
|
229
291
|
};
|
|
230
292
|
nextProviderConfig.accounts = nextAccounts;
|
|
@@ -308,6 +370,7 @@ const createChannelsDomain = ({
|
|
|
308
370
|
const nextName = String(input.name || "").trim();
|
|
309
371
|
const nextAgentId = String(input.agentId || "").trim();
|
|
310
372
|
const nextToken = String(input.token || "").trim();
|
|
373
|
+
const nextAppToken = String(input.appToken || "").trim();
|
|
311
374
|
if (!nextName) throw new Error("Channel name is required");
|
|
312
375
|
if (!nextAgentId) throw new Error("Agent is required");
|
|
313
376
|
|
|
@@ -337,6 +400,41 @@ const createChannelsDomain = ({
|
|
|
337
400
|
}
|
|
338
401
|
|
|
339
402
|
let tokenUpdated = false;
|
|
403
|
+
if (provider === "slack" && nextAppToken) {
|
|
404
|
+
const appEnvKey = deriveChannelExtraEnvKeys({ provider, accountId })[0];
|
|
405
|
+
const currentEnvVars = readEnvFile();
|
|
406
|
+
const previousEnvVars = Array.isArray(currentEnvVars)
|
|
407
|
+
? currentEnvVars
|
|
408
|
+
: [];
|
|
409
|
+
const existingAppToken = String(
|
|
410
|
+
previousEnvVars.find(
|
|
411
|
+
(entry) => String(entry?.key || "").trim() === appEnvKey,
|
|
412
|
+
)?.value || "",
|
|
413
|
+
);
|
|
414
|
+
const duplicateEnvEntry = previousEnvVars.find((entry) => {
|
|
415
|
+
const existingKey = String(entry?.key || "").trim();
|
|
416
|
+
const existingValue = String(entry?.value || "").trim();
|
|
417
|
+
if (!existingKey || !existingValue) return false;
|
|
418
|
+
if (existingKey === appEnvKey) return false;
|
|
419
|
+
return existingValue === nextAppToken;
|
|
420
|
+
});
|
|
421
|
+
if (duplicateEnvEntry) {
|
|
422
|
+
const dupKey = String(duplicateEnvEntry.key || "").trim();
|
|
423
|
+
const configuredKeys = getConfiguredChannelEnvKeys(cfg);
|
|
424
|
+
if (configuredKeys.has(dupKey)) {
|
|
425
|
+
throw new Error(`Channel token already exists in ${dupKey}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (existingAppToken !== nextAppToken) {
|
|
429
|
+
const nextEnvVars = previousEnvVars.filter(
|
|
430
|
+
(entry) => String(entry?.key || "").trim() !== appEnvKey,
|
|
431
|
+
);
|
|
432
|
+
nextEnvVars.push({ key: appEnvKey, value: nextAppToken });
|
|
433
|
+
writeEnvFile(nextEnvVars);
|
|
434
|
+
reloadEnv();
|
|
435
|
+
tokenUpdated = true;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
340
438
|
if (nextToken) {
|
|
341
439
|
const envKey = deriveChannelEnvKey({ provider, accountId });
|
|
342
440
|
const currentEnvVars = readEnvFile();
|
|
@@ -542,12 +640,15 @@ const createChannelsDomain = ({
|
|
|
542
640
|
saveConfig({ fsImpl, OPENCLAW_DIR, config: nextCfg });
|
|
543
641
|
|
|
544
642
|
const envKey = deriveChannelEnvKey({ provider, accountId });
|
|
643
|
+
const extraEnvKeys = deriveChannelExtraEnvKeys({ provider, accountId });
|
|
545
644
|
const currentEnvVars = readEnvFile();
|
|
546
645
|
const previousEnvVars = Array.isArray(currentEnvVars)
|
|
547
646
|
? currentEnvVars
|
|
548
647
|
: [];
|
|
549
648
|
const nextEnvVars = previousEnvVars.filter(
|
|
550
|
-
(entry) =>
|
|
649
|
+
(entry) =>
|
|
650
|
+
String(entry?.key || "").trim() !== envKey &&
|
|
651
|
+
!extraEnvKeys.includes(String(entry?.key || "").trim()),
|
|
551
652
|
);
|
|
552
653
|
if (nextEnvVars.length !== previousEnvVars.length) {
|
|
553
654
|
writeEnvFile(nextEnvVars);
|
|
@@ -577,10 +678,13 @@ const createChannelsDomain = ({
|
|
|
577
678
|
}
|
|
578
679
|
|
|
579
680
|
const envKey = deriveChannelEnvKey({ provider, accountId });
|
|
681
|
+
const extraEnvKeys = deriveChannelExtraEnvKeys({ provider, accountId });
|
|
580
682
|
const currentEnvVars = readEnvFile();
|
|
581
683
|
const previousEnvVars = Array.isArray(currentEnvVars) ? currentEnvVars : [];
|
|
582
684
|
const nextEnvVars = previousEnvVars.filter(
|
|
583
|
-
(entry) =>
|
|
685
|
+
(entry) =>
|
|
686
|
+
String(entry?.key || "").trim() !== envKey &&
|
|
687
|
+
!extraEnvKeys.includes(String(entry?.key || "").trim()),
|
|
584
688
|
);
|
|
585
689
|
if (nextEnvVars.length !== previousEnvVars.length) {
|
|
586
690
|
writeEnvFile(nextEnvVars);
|
|
@@ -13,14 +13,23 @@ const kDefaultAgentFiles = ["SOUL.md", "AGENTS.md", "USER.md", "IDENTITY.md"];
|
|
|
13
13
|
const kChannelEnvKeys = {
|
|
14
14
|
telegram: "TELEGRAM_BOT_TOKEN",
|
|
15
15
|
discord: "DISCORD_BOT_TOKEN",
|
|
16
|
+
slack: "SLACK_BOT_TOKEN",
|
|
17
|
+
};
|
|
18
|
+
const kChannelExtraEnvKeys = {
|
|
19
|
+
slack: ["SLACK_APP_TOKEN"],
|
|
16
20
|
};
|
|
17
21
|
const kChannelTokenFields = {
|
|
18
22
|
telegram: "botToken",
|
|
19
23
|
discord: "token",
|
|
24
|
+
slack: "botToken",
|
|
25
|
+
};
|
|
26
|
+
const kChannelExtraTokenFields = {
|
|
27
|
+
slack: ["appToken"],
|
|
20
28
|
};
|
|
21
29
|
const kChannelLabels = {
|
|
22
30
|
telegram: "Telegram",
|
|
23
31
|
discord: "Discord",
|
|
32
|
+
slack: "Slack",
|
|
24
33
|
};
|
|
25
34
|
const kMaskedChannelToken = "********";
|
|
26
35
|
|
|
@@ -152,6 +161,19 @@ const deriveChannelEnvKey = ({ provider, accountId }) => {
|
|
|
152
161
|
return `${envKey}_${normalizedAccountId.replace(/-/g, "_").toUpperCase()}`;
|
|
153
162
|
};
|
|
154
163
|
|
|
164
|
+
const deriveChannelExtraEnvKeys = ({ provider, accountId }) => {
|
|
165
|
+
const normalizedProvider = normalizeChannelProvider(provider);
|
|
166
|
+
const baseEnvKeys = Array.isArray(kChannelExtraEnvKeys[normalizedProvider])
|
|
167
|
+
? kChannelExtraEnvKeys[normalizedProvider]
|
|
168
|
+
: [];
|
|
169
|
+
const normalizedAccountId = String(accountId || "").trim();
|
|
170
|
+
if (!normalizedAccountId || normalizedAccountId === "default") {
|
|
171
|
+
return [...baseEnvKeys];
|
|
172
|
+
}
|
|
173
|
+
const suffix = normalizedAccountId.replace(/-/g, "_").toUpperCase();
|
|
174
|
+
return baseEnvKeys.map((baseKey) => `${baseKey}_${suffix}`);
|
|
175
|
+
};
|
|
176
|
+
|
|
155
177
|
const getConfiguredChannelEnvKeys = (cfg) => {
|
|
156
178
|
const keys = new Set();
|
|
157
179
|
const channels =
|
|
@@ -164,9 +186,19 @@ const getConfiguredChannelEnvKeys = (cfg) => {
|
|
|
164
186
|
: {};
|
|
165
187
|
for (const accountId of Object.keys(accounts)) {
|
|
166
188
|
keys.add(deriveChannelEnvKey({ provider, accountId }));
|
|
189
|
+
const extraEnvKeys = deriveChannelExtraEnvKeys({ provider, accountId });
|
|
190
|
+
for (const extraEnvKey of extraEnvKeys) {
|
|
191
|
+
keys.add(extraEnvKey);
|
|
192
|
+
}
|
|
167
193
|
}
|
|
168
194
|
if (Object.keys(accounts).length === 0 && providerConfig?.enabled) {
|
|
169
195
|
keys.add(kChannelEnvKeys[provider]);
|
|
196
|
+
for (const extraEnvKey of deriveChannelExtraEnvKeys({
|
|
197
|
+
provider,
|
|
198
|
+
accountId: "default",
|
|
199
|
+
})) {
|
|
200
|
+
keys.add(extraEnvKey);
|
|
201
|
+
}
|
|
170
202
|
}
|
|
171
203
|
}
|
|
172
204
|
return keys;
|
|
@@ -208,6 +240,15 @@ const assertActiveChannelTokenEnvVars = ({ cfg, envVars }) => {
|
|
|
208
240
|
`Missing required channel token env var ${envKey} for active channel ${provider}/${accountId}`,
|
|
209
241
|
);
|
|
210
242
|
}
|
|
243
|
+
const extraEnvKeys = deriveChannelExtraEnvKeys({ provider, accountId });
|
|
244
|
+
for (const extraEnvKey of extraEnvKeys) {
|
|
245
|
+
const extraEnvValue = String(envMap.get(extraEnvKey) || "").trim();
|
|
246
|
+
if (!extraEnvValue) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
`Missing required channel token env var ${extraEnvKey} for active channel ${provider}/${accountId}`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
211
252
|
}
|
|
212
253
|
}
|
|
213
254
|
};
|
|
@@ -223,8 +264,13 @@ const normalizeChannelConfig = ({ provider, channelConfig }) => {
|
|
|
223
264
|
? { ...nextConfig.accounts }
|
|
224
265
|
: {};
|
|
225
266
|
const tokenField = kChannelTokenFields[normalizedProvider];
|
|
267
|
+
const extraTokenFields = Array.isArray(
|
|
268
|
+
kChannelExtraTokenFields[normalizedProvider],
|
|
269
|
+
)
|
|
270
|
+
? kChannelExtraTokenFields[normalizedProvider]
|
|
271
|
+
: [];
|
|
226
272
|
if (Object.keys(existingAccounts).length > 0) {
|
|
227
|
-
if (tokenField) {
|
|
273
|
+
if (tokenField || extraTokenFields.length > 0) {
|
|
228
274
|
for (const [accountId, accountConfig] of Object.entries(
|
|
229
275
|
existingAccounts,
|
|
230
276
|
)) {
|
|
@@ -239,6 +285,17 @@ const normalizeChannelConfig = ({ provider, channelConfig }) => {
|
|
|
239
285
|
accountId,
|
|
240
286
|
})}}`;
|
|
241
287
|
}
|
|
288
|
+
const extraEnvKeys = deriveChannelExtraEnvKeys({
|
|
289
|
+
provider: normalizedProvider,
|
|
290
|
+
accountId,
|
|
291
|
+
});
|
|
292
|
+
extraTokenFields.forEach((fieldName, index) => {
|
|
293
|
+
const rawValue = String(nextAccountConfig[fieldName] || "").trim();
|
|
294
|
+
if (!rawValue) return;
|
|
295
|
+
if (isEnvRef(rawValue)) return;
|
|
296
|
+
if (!extraEnvKeys[index]) return;
|
|
297
|
+
nextAccountConfig[fieldName] = `\${${extraEnvKeys[index]}}`;
|
|
298
|
+
});
|
|
242
299
|
existingAccounts[accountId] = nextAccountConfig;
|
|
243
300
|
}
|
|
244
301
|
}
|
|
@@ -266,6 +323,17 @@ const normalizeChannelConfig = ({ provider, channelConfig }) => {
|
|
|
266
323
|
defaultAccountConfig[tokenField] = defaultTokenEnvRef;
|
|
267
324
|
}
|
|
268
325
|
}
|
|
326
|
+
const defaultExtraEnvKeys = deriveChannelExtraEnvKeys({
|
|
327
|
+
provider: normalizedProvider,
|
|
328
|
+
accountId: "default",
|
|
329
|
+
});
|
|
330
|
+
extraTokenFields.forEach((fieldName, index) => {
|
|
331
|
+
const rawValue = String(defaultAccountConfig[fieldName] || "").trim();
|
|
332
|
+
if (!rawValue) return;
|
|
333
|
+
if (isEnvRef(rawValue)) return;
|
|
334
|
+
if (!defaultExtraEnvKeys[index]) return;
|
|
335
|
+
defaultAccountConfig[fieldName] = `\${${defaultExtraEnvKeys[index]}}`;
|
|
336
|
+
});
|
|
269
337
|
if (
|
|
270
338
|
Object.keys(defaultAccountConfig).length > 0 ||
|
|
271
339
|
defaultAccountConfig[tokenField]
|
|
@@ -615,6 +683,7 @@ module.exports = {
|
|
|
615
683
|
isValidChannelAccountId,
|
|
616
684
|
normalizeChannelProvider,
|
|
617
685
|
deriveChannelEnvKey,
|
|
686
|
+
deriveChannelExtraEnvKeys,
|
|
618
687
|
getConfiguredChannelEnvKeys,
|
|
619
688
|
assertActiveChannelTokenEnvVars,
|
|
620
689
|
normalizeChannelConfig,
|
package/lib/server/constants.js
CHANGED
|
@@ -235,6 +235,18 @@ const kKnownVars = [
|
|
|
235
235
|
group: "channels",
|
|
236
236
|
hint: "From Discord Developer Portal",
|
|
237
237
|
},
|
|
238
|
+
{
|
|
239
|
+
key: "SLACK_BOT_TOKEN",
|
|
240
|
+
label: "Slack Bot Token",
|
|
241
|
+
group: "channels",
|
|
242
|
+
hint: "From your Slack app's OAuth & Permissions page (xoxb-...)",
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
key: "SLACK_APP_TOKEN",
|
|
246
|
+
label: "Slack App Token",
|
|
247
|
+
group: "channels",
|
|
248
|
+
hint: "From Basic Information → App-Level Tokens (xapp-...)",
|
|
249
|
+
},
|
|
238
250
|
{
|
|
239
251
|
key: "MISTRAL_API_KEY",
|
|
240
252
|
label: "Mistral API Key",
|
|
@@ -336,6 +348,7 @@ const API_TEST_COMMANDS = {
|
|
|
336
348
|
const kChannelDefs = {
|
|
337
349
|
telegram: { envKey: "TELEGRAM_BOT_TOKEN" },
|
|
338
350
|
discord: { envKey: "DISCORD_BOT_TOKEN" },
|
|
351
|
+
slack: { envKey: "SLACK_BOT_TOKEN", extraEnvKeys: ["SLACK_APP_TOKEN"] },
|
|
339
352
|
};
|
|
340
353
|
const kProtectedBrowsePaths = new Set(
|
|
341
354
|
Array.isArray(kBrowseFilePolicies?.protectedPaths)
|
package/lib/server/gateway.js
CHANGED
|
@@ -302,17 +302,34 @@ const syncChannelConfig = (savedVars, mode = "all") => {
|
|
|
302
302
|
if (token && !isConfigured && (mode === "add" || mode === "all")) {
|
|
303
303
|
console.log(`[alphaclaw] Adding channel: ${ch}`);
|
|
304
304
|
try {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (raw.includes(token)) {
|
|
312
|
-
fs.writeFileSync(
|
|
313
|
-
configPath,
|
|
314
|
-
raw.split(token).join("${" + def.envKey + "}"),
|
|
305
|
+
if (ch === "slack") {
|
|
306
|
+
const appToken = savedMap[def.extraEnvKeys?.[0]];
|
|
307
|
+
if (!appToken) continue;
|
|
308
|
+
execSync(
|
|
309
|
+
`openclaw channels add --channel slack --bot-token "${token}" --app-token "${appToken}"`,
|
|
310
|
+
{ env, timeout: 15000, encoding: "utf8" },
|
|
315
311
|
);
|
|
312
|
+
let raw = fs.readFileSync(configPath, "utf8");
|
|
313
|
+
if (raw.includes(token)) {
|
|
314
|
+
raw = raw.split(token).join("${" + def.envKey + "}");
|
|
315
|
+
}
|
|
316
|
+
if (raw.includes(appToken)) {
|
|
317
|
+
raw = raw.split(appToken).join("${" + def.extraEnvKeys[0] + "}");
|
|
318
|
+
}
|
|
319
|
+
fs.writeFileSync(configPath, raw);
|
|
320
|
+
} else {
|
|
321
|
+
execSync(`openclaw channels add --channel ${ch} --token "${token}"`, {
|
|
322
|
+
env,
|
|
323
|
+
timeout: 15000,
|
|
324
|
+
encoding: "utf8",
|
|
325
|
+
});
|
|
326
|
+
const raw = fs.readFileSync(configPath, "utf8");
|
|
327
|
+
if (raw.includes(token)) {
|
|
328
|
+
fs.writeFileSync(
|
|
329
|
+
configPath,
|
|
330
|
+
raw.split(token).join("${" + def.envKey + "}"),
|
|
331
|
+
);
|
|
332
|
+
}
|
|
316
333
|
}
|
|
317
334
|
console.log(`[alphaclaw] Channel ${ch} added`);
|
|
318
335
|
} catch (e) {
|
|
@@ -353,7 +370,7 @@ const getChannelStatus = () => {
|
|
|
353
370
|
const credDir = `${OPENCLAW_DIR}/credentials`;
|
|
354
371
|
const channels = {};
|
|
355
372
|
|
|
356
|
-
for (const ch of
|
|
373
|
+
for (const ch of Object.keys(kChannelDefs)) {
|
|
357
374
|
const channelConfig =
|
|
358
375
|
config.channels?.[ch] && typeof config.channels[ch] === "object"
|
|
359
376
|
? config.channels[ch]
|
|
@@ -198,6 +198,19 @@ const applyFreshOnboardingChannels = ({ cfg, varMap }) => {
|
|
|
198
198
|
ensurePluginAllowed(cfg, "discord");
|
|
199
199
|
console.log("[onboard] Discord configured");
|
|
200
200
|
}
|
|
201
|
+
if (varMap.SLACK_BOT_TOKEN && varMap.SLACK_APP_TOKEN) {
|
|
202
|
+
cfg.channels.slack = {
|
|
203
|
+
enabled: true,
|
|
204
|
+
botToken: varMap.SLACK_BOT_TOKEN,
|
|
205
|
+
appToken: varMap.SLACK_APP_TOKEN,
|
|
206
|
+
mode: "socket",
|
|
207
|
+
dmPolicy: "pairing",
|
|
208
|
+
groupPolicy: "open",
|
|
209
|
+
};
|
|
210
|
+
cfg.plugins.entries.slack = { enabled: true };
|
|
211
|
+
ensurePluginAllowed(cfg, "slack");
|
|
212
|
+
console.log("[onboard] Slack configured");
|
|
213
|
+
}
|
|
201
214
|
if (!cfg.plugins.load.paths.includes(kUsageTrackerPluginPath)) {
|
|
202
215
|
cfg.plugins.load.paths.push(kUsageTrackerPluginPath);
|
|
203
216
|
}
|
|
@@ -274,6 +287,23 @@ const writeManagedImportOpenclawConfig = ({ fs, openclawDir, varMap }) => {
|
|
|
274
287
|
ensurePluginAllowed(cfg, "discord");
|
|
275
288
|
}
|
|
276
289
|
|
|
290
|
+
if (varMap.SLACK_BOT_TOKEN && varMap.SLACK_APP_TOKEN) {
|
|
291
|
+
cfg.channels.slack = {
|
|
292
|
+
...(cfg.channels.slack || {}),
|
|
293
|
+
enabled: true,
|
|
294
|
+
botToken: "${SLACK_BOT_TOKEN}",
|
|
295
|
+
appToken: "${SLACK_APP_TOKEN}",
|
|
296
|
+
mode: cfg.channels.slack?.mode || "socket",
|
|
297
|
+
dmPolicy: getSafeImportedDmPolicy(cfg.channels.slack),
|
|
298
|
+
groupPolicy: cfg.channels.slack?.groupPolicy || "open",
|
|
299
|
+
};
|
|
300
|
+
cfg.plugins.entries.slack = {
|
|
301
|
+
...(cfg.plugins.entries.slack || {}),
|
|
302
|
+
enabled: true,
|
|
303
|
+
};
|
|
304
|
+
ensurePluginAllowed(cfg, "slack");
|
|
305
|
+
}
|
|
306
|
+
|
|
277
307
|
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
|
|
278
308
|
};
|
|
279
309
|
|
|
@@ -89,7 +89,7 @@ const validateOnboardingInput = ({ vars, modelKey, resolveModelProvider, hasCode
|
|
|
89
89
|
? hasAiByProvider[selectedProvider]
|
|
90
90
|
: hasAnyAi;
|
|
91
91
|
const hasGithub = !!(githubToken && githubRepoInput);
|
|
92
|
-
const hasChannel = !!(varMap.TELEGRAM_BOT_TOKEN || varMap.DISCORD_BOT_TOKEN);
|
|
92
|
+
const hasChannel = !!(varMap.TELEGRAM_BOT_TOKEN || varMap.DISCORD_BOT_TOKEN || (varMap.SLACK_BOT_TOKEN && varMap.SLACK_APP_TOKEN));
|
|
93
93
|
|
|
94
94
|
if (!hasAi) {
|
|
95
95
|
if (selectedProvider === "openai-codex") {
|
|
@@ -5,7 +5,7 @@ const { buildManagedPaths } = require("../internal-files-migration");
|
|
|
5
5
|
const { parseJsonObjectFromNoisyOutput } = require("../utils/json");
|
|
6
6
|
const { quoteShellArg } = require("../utils/shell");
|
|
7
7
|
|
|
8
|
-
const kAllowedPairingChannels = new Set(["telegram", "discord"]);
|
|
8
|
+
const kAllowedPairingChannels = new Set(["telegram", "discord", "slack"]);
|
|
9
9
|
const kSafePairingArgPattern = /^[\w\-:.]+$/;
|
|
10
10
|
const quoteCliArg = (value) => quoteShellArg(value, { strategy: "single" });
|
|
11
11
|
|
|
@@ -111,7 +111,7 @@ const registerPairingRoutes = ({ app, clawCmd, isOnboarded, fsModule = fs, openc
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
const pending = [];
|
|
114
|
-
const channels = ["telegram", "discord"];
|
|
114
|
+
const channels = ["telegram", "discord", "slack"];
|
|
115
115
|
|
|
116
116
|
for (const ch of channels) {
|
|
117
117
|
try {
|
|
@@ -25,7 +25,8 @@ const registerSystemRoutes = ({
|
|
|
25
25
|
authProfiles,
|
|
26
26
|
}) => {
|
|
27
27
|
let envRestartPending = false;
|
|
28
|
-
const kManagedChannelTokenPattern =
|
|
28
|
+
const kManagedChannelTokenPattern =
|
|
29
|
+
/^(?:TELEGRAM_BOT_TOKEN|DISCORD_BOT_TOKEN|SLACK_BOT_TOKEN|SLACK_APP_TOKEN)(?:_[A-Z0-9_]+)?$/;
|
|
29
30
|
const kEnvVarsReservedForUserInput = new Set([
|
|
30
31
|
"GITHUB_WORKSPACE_REPO",
|
|
31
32
|
"GOG_KEYRING_PASSWORD",
|
|
@@ -311,7 +312,13 @@ const registerSystemRoutes = ({
|
|
|
311
312
|
}
|
|
312
313
|
|
|
313
314
|
for (const v of fileVars) {
|
|
314
|
-
if (
|
|
315
|
+
if (
|
|
316
|
+
kKnownKeys.has(v.key) ||
|
|
317
|
+
isReservedUserEnvVar(v.key) ||
|
|
318
|
+
isManagedChannelTokenKey(v.key)
|
|
319
|
+
) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
315
322
|
merged.push({
|
|
316
323
|
key: v.key,
|
|
317
324
|
value: v.value,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const kSlackApiBase = "https://slack.com/api";
|
|
2
|
+
|
|
3
|
+
const createSlackApi = (getToken) => {
|
|
4
|
+
const call = async (method, body = {}) => {
|
|
5
|
+
const token = typeof getToken === "function" ? getToken() : getToken;
|
|
6
|
+
if (!token) throw new Error("SLACK_BOT_TOKEN is not set");
|
|
7
|
+
const res = await fetch(`${kSlackApiBase}/${method}`, {
|
|
8
|
+
method: "POST",
|
|
9
|
+
headers: {
|
|
10
|
+
Authorization: `Bearer ${token}`,
|
|
11
|
+
"Content-Type": "application/json",
|
|
12
|
+
},
|
|
13
|
+
body: JSON.stringify(body),
|
|
14
|
+
});
|
|
15
|
+
if (!res.ok) {
|
|
16
|
+
throw new Error(`Slack API ${method}: HTTP ${res.status}`);
|
|
17
|
+
}
|
|
18
|
+
const data = await res.json();
|
|
19
|
+
if (!data.ok) {
|
|
20
|
+
const err = new Error(data.error || `Slack API error: ${method}`);
|
|
21
|
+
err.slackError = data.error;
|
|
22
|
+
throw err;
|
|
23
|
+
}
|
|
24
|
+
return data;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const authTest = () => call("auth.test");
|
|
28
|
+
|
|
29
|
+
const postMessage = (channel, text) =>
|
|
30
|
+
call("chat.postMessage", { channel, text: String(text || "") });
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
authTest,
|
|
34
|
+
postMessage,
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
module.exports = { createSlackApi };
|
|
@@ -36,11 +36,12 @@ const getPairedIds = (channel) => {
|
|
|
36
36
|
const formatDiscordMessage = (message) =>
|
|
37
37
|
String(message || "").replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, "**$1**");
|
|
38
38
|
|
|
39
|
-
const createWatchdogNotifier = ({ telegramApi, discordApi }) => {
|
|
39
|
+
const createWatchdogNotifier = ({ telegramApi, discordApi, slackApi }) => {
|
|
40
40
|
const notify = async (message) => {
|
|
41
41
|
const summary = {
|
|
42
42
|
telegram: { sent: 0, failed: 0, skipped: false, targets: 0 },
|
|
43
43
|
discord: { sent: 0, failed: 0, skipped: false, targets: 0 },
|
|
44
|
+
slack: { sent: 0, failed: 0, skipped: false, targets: 0 },
|
|
44
45
|
};
|
|
45
46
|
const telegramTargets = getPairedIds("telegram");
|
|
46
47
|
summary.telegram.targets = telegramTargets.length;
|
|
@@ -77,8 +78,24 @@ const createWatchdogNotifier = ({ telegramApi, discordApi }) => {
|
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
const
|
|
81
|
-
|
|
81
|
+
const slackTargets = getPairedIds("slack");
|
|
82
|
+
summary.slack.targets = slackTargets.length;
|
|
83
|
+
if (!slackApi?.postMessage || !process.env.SLACK_BOT_TOKEN || slackTargets.length === 0) {
|
|
84
|
+
summary.slack.skipped = true;
|
|
85
|
+
} else {
|
|
86
|
+
for (const userId of slackTargets) {
|
|
87
|
+
try {
|
|
88
|
+
await slackApi.postMessage(userId, String(message || ""));
|
|
89
|
+
summary.slack.sent += 1;
|
|
90
|
+
} catch (err) {
|
|
91
|
+
summary.slack.failed += 1;
|
|
92
|
+
console.error(`[watchdog] slack notification failed for ${userId}: ${err.message}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const sent = summary.telegram.sent + summary.discord.sent + summary.slack.sent;
|
|
98
|
+
const failed = summary.telegram.failed + summary.discord.failed + summary.slack.failed;
|
|
82
99
|
return {
|
|
83
100
|
ok: sent > 0,
|
|
84
101
|
sent,
|
package/lib/server.js
CHANGED
|
@@ -104,6 +104,7 @@ const {
|
|
|
104
104
|
const { installGogCliSkill } = require("./server/gog-skill");
|
|
105
105
|
const { createTelegramApi } = require("./server/telegram-api");
|
|
106
106
|
const { createDiscordApi } = require("./server/discord-api");
|
|
107
|
+
const { createSlackApi } = require("./server/slack-api");
|
|
107
108
|
const { createWatchdogNotifier } = require("./server/watchdog-notify");
|
|
108
109
|
const { createWatchdog } = require("./server/watchdog");
|
|
109
110
|
const { createDoctorService } = require("./server/doctor/service");
|
|
@@ -307,7 +308,8 @@ const gmailWatchService = registerGmailRoutes({
|
|
|
307
308
|
});
|
|
308
309
|
const telegramApi = createTelegramApi(() => process.env.TELEGRAM_BOT_TOKEN);
|
|
309
310
|
const discordApi = createDiscordApi(() => process.env.DISCORD_BOT_TOKEN);
|
|
310
|
-
const
|
|
311
|
+
const slackApi = createSlackApi(() => process.env.SLACK_BOT_TOKEN);
|
|
312
|
+
const watchdogNotifier = createWatchdogNotifier({ telegramApi, discordApi, slackApi });
|
|
311
313
|
const watchdog = createWatchdog({
|
|
312
314
|
clawCmd,
|
|
313
315
|
launchGatewayProcess,
|