@chrysb/alphaclaw 0.6.0-beta.0 → 0.6.0-beta.2
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/js/components/agents-tab/agent-bindings-section/channel-item-trailing.js +203 -0
- package/lib/public/js/components/agents-tab/agent-bindings-section/helpers.js +10 -12
- package/lib/public/js/components/agents-tab/agent-bindings-section/index.js +19 -287
- package/lib/public/js/components/agents-tab/agent-bindings-section/use-agent-bindings.js +1 -1
- package/lib/public/js/components/agents-tab/agent-bindings-section/use-channel-items.js +211 -0
- package/lib/public/js/components/agents-tab/agent-pairing-section.js +17 -4
- package/lib/public/js/components/agents-tab/create-channel-modal.js +29 -6
- package/lib/public/js/components/channels.js +19 -14
- package/lib/public/js/components/doctor/index.js +2 -1
- package/lib/public/js/lib/channel-accounts.js +20 -0
- package/lib/server/agents/agents.js +207 -0
- package/lib/server/agents/bindings.js +74 -0
- package/lib/server/agents/channels.js +674 -0
- package/lib/server/agents/service.js +28 -1457
- package/lib/server/agents/shared.js +631 -0
- package/lib/server/constants.js +1 -0
- package/lib/server/openclaw-config.js +13 -0
- package/lib/server/routes/pairings.js +29 -3
- package/lib/server/routes/system.js +1 -6
- package/lib/server/routes/telegram.js +34 -16
- package/lib/server/telegram-workspace.js +16 -5
- package/lib/server/topic-registry.js +1 -4
- package/lib/server/utils/channels.js +13 -0
- package/package.json +2 -2
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
kChannelTokenFields,
|
|
5
|
+
kChannelLabels,
|
|
6
|
+
kMaskedChannelToken,
|
|
7
|
+
shellEscapeArg,
|
|
8
|
+
resolveCredentialsDirPath,
|
|
9
|
+
loadConfig,
|
|
10
|
+
saveConfig,
|
|
11
|
+
ensurePluginAllowed,
|
|
12
|
+
cloneJson,
|
|
13
|
+
normalizeBindingMatch,
|
|
14
|
+
matchesBinding,
|
|
15
|
+
isValidChannelAccountId,
|
|
16
|
+
normalizeChannelProvider,
|
|
17
|
+
deriveChannelEnvKey,
|
|
18
|
+
getConfiguredChannelEnvKeys,
|
|
19
|
+
assertActiveChannelTokenEnvVars,
|
|
20
|
+
normalizeChannelConfig,
|
|
21
|
+
appendBindingToConfig,
|
|
22
|
+
buildBindingSpec,
|
|
23
|
+
hasLegacyDefaultChannelAccount,
|
|
24
|
+
listConfiguredChannelAccounts,
|
|
25
|
+
withNormalizedAgentsConfig,
|
|
26
|
+
} = require("./shared");
|
|
27
|
+
|
|
28
|
+
const createChannelsDomain = ({
|
|
29
|
+
fsImpl,
|
|
30
|
+
OPENCLAW_DIR,
|
|
31
|
+
readEnvFile,
|
|
32
|
+
writeEnvFile,
|
|
33
|
+
reloadEnv,
|
|
34
|
+
restartGateway,
|
|
35
|
+
clawCmd,
|
|
36
|
+
}) => {
|
|
37
|
+
let createChannelAccountInProgress = false;
|
|
38
|
+
|
|
39
|
+
const getChannelAccountToken = ({
|
|
40
|
+
provider: rawProvider,
|
|
41
|
+
accountId: rawAccountId,
|
|
42
|
+
} = {}) => {
|
|
43
|
+
const provider = normalizeChannelProvider(rawProvider);
|
|
44
|
+
const accountId = String(rawAccountId || "").trim() || "default";
|
|
45
|
+
const cfg = withNormalizedAgentsConfig({
|
|
46
|
+
OPENCLAW_DIR,
|
|
47
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
48
|
+
});
|
|
49
|
+
const providerConfig =
|
|
50
|
+
cfg.channels?.[provider] && typeof cfg.channels[provider] === "object"
|
|
51
|
+
? cfg.channels[provider]
|
|
52
|
+
: null;
|
|
53
|
+
if (!providerConfig) {
|
|
54
|
+
throw new Error(`Channel "${provider}" not found`);
|
|
55
|
+
}
|
|
56
|
+
const hasAccounts =
|
|
57
|
+
providerConfig.accounts && typeof providerConfig.accounts === "object";
|
|
58
|
+
const hasLegacyDefault =
|
|
59
|
+
accountId === "default" &&
|
|
60
|
+
!hasAccounts &&
|
|
61
|
+
hasLegacyDefaultChannelAccount({ config: providerConfig });
|
|
62
|
+
if (!hasLegacyDefault && !providerConfig.accounts?.[accountId]) {
|
|
63
|
+
throw new Error(`Channel account "${provider}/${accountId}" not found`);
|
|
64
|
+
}
|
|
65
|
+
const envKey = deriveChannelEnvKey({ provider, accountId });
|
|
66
|
+
const envVars = readEnvFile();
|
|
67
|
+
const envEntry = (Array.isArray(envVars) ? envVars : []).find(
|
|
68
|
+
(entry) => String(entry?.key || "").trim() === envKey,
|
|
69
|
+
);
|
|
70
|
+
return {
|
|
71
|
+
provider,
|
|
72
|
+
accountId,
|
|
73
|
+
envKey,
|
|
74
|
+
token: String(envEntry?.value || ""),
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const createChannelAccount = async (
|
|
79
|
+
input = {},
|
|
80
|
+
{ onProgress = () => {} } = {},
|
|
81
|
+
) => {
|
|
82
|
+
if (createChannelAccountInProgress) {
|
|
83
|
+
throw new Error("A channel account creation is already in progress");
|
|
84
|
+
}
|
|
85
|
+
createChannelAccountInProgress = true;
|
|
86
|
+
try {
|
|
87
|
+
const provider = normalizeChannelProvider(input.provider);
|
|
88
|
+
const name =
|
|
89
|
+
String(input.name || "").trim() || kChannelLabels[provider] || provider;
|
|
90
|
+
const token = String(input.token || "").trim();
|
|
91
|
+
if (!token) throw new Error("Channel token is required");
|
|
92
|
+
|
|
93
|
+
const cfg = withNormalizedAgentsConfig({
|
|
94
|
+
OPENCLAW_DIR,
|
|
95
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const agentId = String(input.agentId || "").trim();
|
|
99
|
+
const agent = cfg.agents.list.find((entry) => entry.id === agentId);
|
|
100
|
+
if (!agent) throw new Error(`Agent "${agentId}" not found`);
|
|
101
|
+
|
|
102
|
+
const existingChannelConfig =
|
|
103
|
+
cfg.channels?.[provider] && typeof cfg.channels[provider] === "object"
|
|
104
|
+
? cfg.channels[provider]
|
|
105
|
+
: {};
|
|
106
|
+
const normalizedChannelConfig = normalizeChannelConfig({
|
|
107
|
+
provider,
|
|
108
|
+
channelConfig: existingChannelConfig,
|
|
109
|
+
});
|
|
110
|
+
const existingAccounts =
|
|
111
|
+
normalizedChannelConfig.accounts &&
|
|
112
|
+
typeof normalizedChannelConfig.accounts === "object"
|
|
113
|
+
? normalizedChannelConfig.accounts
|
|
114
|
+
: {};
|
|
115
|
+
const requestedAccountId = String(input.accountId || "").trim();
|
|
116
|
+
const accountId =
|
|
117
|
+
requestedAccountId ||
|
|
118
|
+
(Object.keys(existingAccounts).length > 0 ? "" : "default");
|
|
119
|
+
if (!accountId) {
|
|
120
|
+
throw new Error("Channel account id is required");
|
|
121
|
+
}
|
|
122
|
+
if (!isValidChannelAccountId(accountId)) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
"Channel account id must be lowercase letters, numbers, and hyphens only",
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (existingAccounts[accountId]) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Channel account "${provider}/${accountId}" already exists`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if (provider === "discord" && Object.keys(existingAccounts).length > 0) {
|
|
133
|
+
throw new Error("Discord supports a single channel account");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const envKey = deriveChannelEnvKey({ provider, accountId });
|
|
137
|
+
const tokenField = kChannelTokenFields[provider];
|
|
138
|
+
const currentEnvVars = readEnvFile();
|
|
139
|
+
const previousEnvVars = Array.isArray(currentEnvVars) ? currentEnvVars : [];
|
|
140
|
+
const duplicateEnvEntry = previousEnvVars.find((entry) => {
|
|
141
|
+
const existingKey = String(entry?.key || "").trim();
|
|
142
|
+
const existingValue = String(entry?.value || "").trim();
|
|
143
|
+
if (!existingKey || !existingValue) return false;
|
|
144
|
+
if (existingKey === envKey) return false;
|
|
145
|
+
return existingValue === token;
|
|
146
|
+
});
|
|
147
|
+
let orphanedEnvKey = null;
|
|
148
|
+
if (duplicateEnvEntry) {
|
|
149
|
+
const dupKey = String(duplicateEnvEntry.key || "").trim();
|
|
150
|
+
const configuredKeys = getConfiguredChannelEnvKeys(cfg);
|
|
151
|
+
if (configuredKeys.has(dupKey)) {
|
|
152
|
+
throw new Error(`Channel token already exists in ${dupKey}`);
|
|
153
|
+
}
|
|
154
|
+
orphanedEnvKey = dupKey;
|
|
155
|
+
console.log(
|
|
156
|
+
`[alphaclaw] Overwriting orphaned channel env var ${dupKey} (no matching config entry)`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const nextEnvVars = previousEnvVars.filter((entry) => {
|
|
160
|
+
const key = String(entry?.key || "").trim();
|
|
161
|
+
return key !== envKey && key !== orphanedEnvKey;
|
|
162
|
+
});
|
|
163
|
+
nextEnvVars.push({ key: envKey, value: token });
|
|
164
|
+
|
|
165
|
+
const previousConfig = cloneJson(cfg);
|
|
166
|
+
try {
|
|
167
|
+
onProgress({ phase: "restarting", label: "Rebooting..." });
|
|
168
|
+
writeEnvFile(nextEnvVars);
|
|
169
|
+
reloadEnv();
|
|
170
|
+
assertActiveChannelTokenEnvVars({
|
|
171
|
+
cfg: withNormalizedAgentsConfig({
|
|
172
|
+
OPENCLAW_DIR,
|
|
173
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
174
|
+
}),
|
|
175
|
+
envVars: nextEnvVars,
|
|
176
|
+
});
|
|
177
|
+
await restartGateway();
|
|
178
|
+
const pluginEnabledCfg = withNormalizedAgentsConfig({
|
|
179
|
+
OPENCLAW_DIR,
|
|
180
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
181
|
+
});
|
|
182
|
+
ensurePluginAllowed({ cfg: pluginEnabledCfg, pluginKey: provider });
|
|
183
|
+
saveConfig({ fsImpl, OPENCLAW_DIR, config: pluginEnabledCfg });
|
|
184
|
+
const addArgs = [
|
|
185
|
+
"channels add",
|
|
186
|
+
`--channel ${shellEscapeArg(provider)}`,
|
|
187
|
+
accountId !== "default"
|
|
188
|
+
? `--account ${shellEscapeArg(accountId)}`
|
|
189
|
+
: "",
|
|
190
|
+
name ? `--name ${shellEscapeArg(name)}` : "",
|
|
191
|
+
`--token ${shellEscapeArg(token)}`,
|
|
192
|
+
].filter(Boolean);
|
|
193
|
+
const addResult = await clawCmd(addArgs.join(" "), {
|
|
194
|
+
quiet: true,
|
|
195
|
+
timeoutMs: 30000,
|
|
196
|
+
});
|
|
197
|
+
if (!addResult?.ok) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
addResult?.stderr ||
|
|
200
|
+
addResult?.stdout ||
|
|
201
|
+
"Could not add channel account",
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
const nextCfg = withNormalizedAgentsConfig({
|
|
205
|
+
OPENCLAW_DIR,
|
|
206
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
207
|
+
});
|
|
208
|
+
const nextProviderConfig = normalizeChannelConfig({
|
|
209
|
+
provider,
|
|
210
|
+
channelConfig:
|
|
211
|
+
nextCfg.channels?.[provider] &&
|
|
212
|
+
typeof nextCfg.channels[provider] === "object"
|
|
213
|
+
? nextCfg.channels[provider]
|
|
214
|
+
: {},
|
|
215
|
+
});
|
|
216
|
+
const nextAccounts =
|
|
217
|
+
nextProviderConfig.accounts &&
|
|
218
|
+
typeof nextProviderConfig.accounts === "object"
|
|
219
|
+
? { ...nextProviderConfig.accounts }
|
|
220
|
+
: {};
|
|
221
|
+
nextAccounts[accountId] = {
|
|
222
|
+
...(nextAccounts[accountId] &&
|
|
223
|
+
typeof nextAccounts[accountId] === "object"
|
|
224
|
+
? nextAccounts[accountId]
|
|
225
|
+
: {}),
|
|
226
|
+
...(name ? { name } : {}),
|
|
227
|
+
[tokenField]: `\${${envKey}}`,
|
|
228
|
+
dmPolicy: "pairing",
|
|
229
|
+
};
|
|
230
|
+
nextProviderConfig.accounts = nextAccounts;
|
|
231
|
+
nextProviderConfig.enabled = true;
|
|
232
|
+
if (
|
|
233
|
+
nextProviderConfig.accounts &&
|
|
234
|
+
typeof nextProviderConfig.accounts === "object" &&
|
|
235
|
+
!String(nextProviderConfig.defaultAccount || "").trim()
|
|
236
|
+
) {
|
|
237
|
+
nextProviderConfig.defaultAccount = "default";
|
|
238
|
+
}
|
|
239
|
+
nextCfg.channels =
|
|
240
|
+
nextCfg.channels && typeof nextCfg.channels === "object"
|
|
241
|
+
? { ...nextCfg.channels }
|
|
242
|
+
: {};
|
|
243
|
+
nextCfg.channels[provider] = nextProviderConfig;
|
|
244
|
+
saveConfig({ fsImpl, OPENCLAW_DIR, config: nextCfg });
|
|
245
|
+
onProgress({ phase: "binding", label: "Binding agent..." });
|
|
246
|
+
const bindSpec = buildBindingSpec({ provider, accountId });
|
|
247
|
+
const bindResult = await clawCmd(
|
|
248
|
+
`agents bind --agent ${shellEscapeArg(agentId)} --bind ${shellEscapeArg(bindSpec)}`,
|
|
249
|
+
{ quiet: true, timeoutMs: 30000 },
|
|
250
|
+
);
|
|
251
|
+
if (!bindResult?.ok) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
bindResult?.stderr ||
|
|
254
|
+
bindResult?.stdout ||
|
|
255
|
+
"Could not bind channel account",
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
} catch (error) {
|
|
259
|
+
try {
|
|
260
|
+
await clawCmd(
|
|
261
|
+
[
|
|
262
|
+
"channels remove",
|
|
263
|
+
`--channel ${shellEscapeArg(provider)}`,
|
|
264
|
+
accountId !== "default"
|
|
265
|
+
? `--account ${shellEscapeArg(accountId)}`
|
|
266
|
+
: "",
|
|
267
|
+
"--delete",
|
|
268
|
+
]
|
|
269
|
+
.filter(Boolean)
|
|
270
|
+
.join(" "),
|
|
271
|
+
{ quiet: true, timeoutMs: 30000 },
|
|
272
|
+
);
|
|
273
|
+
} catch {}
|
|
274
|
+
try {
|
|
275
|
+
writeEnvFile(previousEnvVars);
|
|
276
|
+
reloadEnv();
|
|
277
|
+
} catch {}
|
|
278
|
+
try {
|
|
279
|
+
saveConfig({ fsImpl, OPENCLAW_DIR, config: previousConfig });
|
|
280
|
+
} catch {}
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const binding = {
|
|
285
|
+
agentId,
|
|
286
|
+
match: normalizeBindingMatch({
|
|
287
|
+
channel: provider,
|
|
288
|
+
accountId,
|
|
289
|
+
}),
|
|
290
|
+
};
|
|
291
|
+
return {
|
|
292
|
+
channel: provider,
|
|
293
|
+
account: {
|
|
294
|
+
id: accountId,
|
|
295
|
+
name,
|
|
296
|
+
envKey,
|
|
297
|
+
},
|
|
298
|
+
binding,
|
|
299
|
+
};
|
|
300
|
+
} finally {
|
|
301
|
+
createChannelAccountInProgress = false;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const updateChannelAccount = (input = {}) => {
|
|
306
|
+
const provider = normalizeChannelProvider(input.provider);
|
|
307
|
+
const accountId = String(input.accountId || "").trim() || "default";
|
|
308
|
+
const nextName = String(input.name || "").trim();
|
|
309
|
+
const nextAgentId = String(input.agentId || "").trim();
|
|
310
|
+
const nextToken = String(input.token || "").trim();
|
|
311
|
+
if (!nextName) throw new Error("Channel name is required");
|
|
312
|
+
if (!nextAgentId) throw new Error("Agent is required");
|
|
313
|
+
|
|
314
|
+
const cfg = withNormalizedAgentsConfig({
|
|
315
|
+
OPENCLAW_DIR,
|
|
316
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
317
|
+
});
|
|
318
|
+
const agent = cfg.agents.list.find((entry) => entry.id === nextAgentId);
|
|
319
|
+
if (!agent) throw new Error(`Agent "${nextAgentId}" not found`);
|
|
320
|
+
|
|
321
|
+
const providerConfig =
|
|
322
|
+
cfg.channels?.[provider] && typeof cfg.channels[provider] === "object"
|
|
323
|
+
? { ...cfg.channels[provider] }
|
|
324
|
+
: null;
|
|
325
|
+
if (!providerConfig) {
|
|
326
|
+
throw new Error(`Channel "${provider}" not found`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const hasAccounts =
|
|
330
|
+
providerConfig.accounts && typeof providerConfig.accounts === "object";
|
|
331
|
+
const hasLegacyDefault =
|
|
332
|
+
accountId === "default" &&
|
|
333
|
+
!hasAccounts &&
|
|
334
|
+
hasLegacyDefaultChannelAccount({ config: providerConfig });
|
|
335
|
+
if (!hasLegacyDefault && !providerConfig.accounts?.[accountId]) {
|
|
336
|
+
throw new Error(`Channel account "${provider}/${accountId}" not found`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
let tokenUpdated = false;
|
|
340
|
+
if (nextToken) {
|
|
341
|
+
const envKey = deriveChannelEnvKey({ provider, accountId });
|
|
342
|
+
const currentEnvVars = readEnvFile();
|
|
343
|
+
const previousEnvVars = Array.isArray(currentEnvVars)
|
|
344
|
+
? currentEnvVars
|
|
345
|
+
: [];
|
|
346
|
+
const existingToken = String(
|
|
347
|
+
previousEnvVars.find(
|
|
348
|
+
(entry) => String(entry?.key || "").trim() === envKey,
|
|
349
|
+
)?.value || "",
|
|
350
|
+
);
|
|
351
|
+
const duplicateEnvEntry = previousEnvVars.find((entry) => {
|
|
352
|
+
const existingKey = String(entry?.key || "").trim();
|
|
353
|
+
const existingValue = String(entry?.value || "").trim();
|
|
354
|
+
if (!existingKey || !existingValue) return false;
|
|
355
|
+
if (existingKey === envKey) return false;
|
|
356
|
+
return existingValue === nextToken;
|
|
357
|
+
});
|
|
358
|
+
if (duplicateEnvEntry) {
|
|
359
|
+
const dupKey = String(duplicateEnvEntry.key || "").trim();
|
|
360
|
+
const configuredKeys = getConfiguredChannelEnvKeys(cfg);
|
|
361
|
+
if (configuredKeys.has(dupKey)) {
|
|
362
|
+
throw new Error(`Channel token already exists in ${dupKey}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (existingToken !== nextToken) {
|
|
366
|
+
const nextEnvVars = previousEnvVars.filter(
|
|
367
|
+
(entry) => String(entry?.key || "").trim() !== envKey,
|
|
368
|
+
);
|
|
369
|
+
nextEnvVars.push({ key: envKey, value: nextToken });
|
|
370
|
+
writeEnvFile(nextEnvVars);
|
|
371
|
+
reloadEnv();
|
|
372
|
+
tokenUpdated = true;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (hasLegacyDefault) {
|
|
377
|
+
providerConfig.name = nextName;
|
|
378
|
+
} else {
|
|
379
|
+
providerConfig.accounts = { ...providerConfig.accounts };
|
|
380
|
+
providerConfig.accounts[accountId] = {
|
|
381
|
+
...(providerConfig.accounts[accountId] || {}),
|
|
382
|
+
name: nextName,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
cfg.channels =
|
|
386
|
+
cfg.channels && typeof cfg.channels === "object"
|
|
387
|
+
? { ...cfg.channels }
|
|
388
|
+
: {};
|
|
389
|
+
cfg.channels[provider] = providerConfig;
|
|
390
|
+
|
|
391
|
+
const bindings = Array.isArray(cfg.bindings) ? cfg.bindings : [];
|
|
392
|
+
const targetMatch = normalizeBindingMatch({ channel: provider, accountId });
|
|
393
|
+
const nextBindings = bindings.filter((binding) => {
|
|
394
|
+
const match = binding?.match || {};
|
|
395
|
+
const hasScopedFields =
|
|
396
|
+
!!match.peer ||
|
|
397
|
+
!!match.parentPeer ||
|
|
398
|
+
!!String(match.guildId || "").trim() ||
|
|
399
|
+
!!String(match.teamId || "").trim() ||
|
|
400
|
+
(Array.isArray(match.roles) && match.roles.length > 0);
|
|
401
|
+
if (hasScopedFields) return true;
|
|
402
|
+
return !matchesBinding(match, targetMatch);
|
|
403
|
+
});
|
|
404
|
+
cfg.bindings = nextBindings;
|
|
405
|
+
appendBindingToConfig({
|
|
406
|
+
cfg,
|
|
407
|
+
agentId: nextAgentId,
|
|
408
|
+
match: targetMatch,
|
|
409
|
+
});
|
|
410
|
+
saveConfig({ fsImpl, OPENCLAW_DIR, config: cfg });
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
channel: provider,
|
|
414
|
+
account: {
|
|
415
|
+
id: accountId,
|
|
416
|
+
name: nextName,
|
|
417
|
+
boundAgentId: nextAgentId,
|
|
418
|
+
},
|
|
419
|
+
tokenUpdated,
|
|
420
|
+
};
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const cleanupChannelAccountPairingFiles = ({ provider, accountId }) => {
|
|
424
|
+
const credDir = resolveCredentialsDirPath({ OPENCLAW_DIR });
|
|
425
|
+
const normalizedAccountId =
|
|
426
|
+
String(accountId || "")
|
|
427
|
+
.trim()
|
|
428
|
+
.toLowerCase() || "default";
|
|
429
|
+
|
|
430
|
+
const pairingFilePath = path.join(credDir, `${provider}-pairing.json`);
|
|
431
|
+
try {
|
|
432
|
+
const raw = fsImpl.readFileSync(pairingFilePath, "utf8");
|
|
433
|
+
const parsed = JSON.parse(raw);
|
|
434
|
+
const requests = Array.isArray(parsed?.requests) ? parsed.requests : [];
|
|
435
|
+
const nextRequests = requests.filter((entry) => {
|
|
436
|
+
const entryAccountId =
|
|
437
|
+
String(entry?.meta?.accountId || "")
|
|
438
|
+
.trim()
|
|
439
|
+
.toLowerCase() || "default";
|
|
440
|
+
return entryAccountId !== normalizedAccountId;
|
|
441
|
+
});
|
|
442
|
+
if (nextRequests.length !== requests.length) {
|
|
443
|
+
fsImpl.writeFileSync(
|
|
444
|
+
pairingFilePath,
|
|
445
|
+
JSON.stringify({ version: 1, requests: nextRequests }, null, 2),
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
} catch {}
|
|
449
|
+
|
|
450
|
+
const allowFromPatterns = [
|
|
451
|
+
`${provider}-${normalizedAccountId}-allowFrom.json`,
|
|
452
|
+
...(normalizedAccountId === "default"
|
|
453
|
+
? [`${provider}-allowFrom.json`]
|
|
454
|
+
: []),
|
|
455
|
+
];
|
|
456
|
+
for (const fileName of allowFromPatterns) {
|
|
457
|
+
try {
|
|
458
|
+
fsImpl.rmSync(path.join(credDir, fileName), { force: true });
|
|
459
|
+
} catch {}
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const deleteChannelAccount = async (input = {}) => {
|
|
464
|
+
const provider = normalizeChannelProvider(input.provider);
|
|
465
|
+
const accountId = String(input.accountId || "").trim() || "default";
|
|
466
|
+
|
|
467
|
+
const cfg = withNormalizedAgentsConfig({
|
|
468
|
+
OPENCLAW_DIR,
|
|
469
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
470
|
+
});
|
|
471
|
+
const providerConfig =
|
|
472
|
+
cfg.channels?.[provider] && typeof cfg.channels[provider] === "object"
|
|
473
|
+
? cfg.channels[provider]
|
|
474
|
+
: null;
|
|
475
|
+
if (!providerConfig) {
|
|
476
|
+
throw new Error(`Channel "${provider}" not found`);
|
|
477
|
+
}
|
|
478
|
+
const hasAccounts =
|
|
479
|
+
providerConfig.accounts && typeof providerConfig.accounts === "object";
|
|
480
|
+
const hasLegacyDefault =
|
|
481
|
+
accountId === "default" &&
|
|
482
|
+
!hasAccounts &&
|
|
483
|
+
hasLegacyDefaultChannelAccount({ config: providerConfig });
|
|
484
|
+
if (!hasLegacyDefault && !providerConfig.accounts?.[accountId]) {
|
|
485
|
+
throw new Error(`Channel account "${provider}/${accountId}" not found`);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (provider === "discord") {
|
|
489
|
+
const nextCfg = withNormalizedAgentsConfig({
|
|
490
|
+
OPENCLAW_DIR,
|
|
491
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
492
|
+
});
|
|
493
|
+
const nextChannels =
|
|
494
|
+
nextCfg.channels && typeof nextCfg.channels === "object"
|
|
495
|
+
? { ...nextCfg.channels }
|
|
496
|
+
: {};
|
|
497
|
+
const nextProviderConfig = normalizeChannelConfig({
|
|
498
|
+
provider,
|
|
499
|
+
channelConfig:
|
|
500
|
+
nextChannels[provider] && typeof nextChannels[provider] === "object"
|
|
501
|
+
? nextChannels[provider]
|
|
502
|
+
: {},
|
|
503
|
+
});
|
|
504
|
+
const nextAccounts =
|
|
505
|
+
nextProviderConfig.accounts &&
|
|
506
|
+
typeof nextProviderConfig.accounts === "object"
|
|
507
|
+
? { ...nextProviderConfig.accounts }
|
|
508
|
+
: {};
|
|
509
|
+
delete nextAccounts[accountId];
|
|
510
|
+
if (Object.keys(nextAccounts).length > 0) {
|
|
511
|
+
nextProviderConfig.accounts = nextAccounts;
|
|
512
|
+
nextChannels[provider] = nextProviderConfig;
|
|
513
|
+
} else {
|
|
514
|
+
delete nextChannels[provider];
|
|
515
|
+
}
|
|
516
|
+
nextCfg.channels = nextChannels;
|
|
517
|
+
|
|
518
|
+
const targetMatch = normalizeBindingMatch({
|
|
519
|
+
channel: provider,
|
|
520
|
+
accountId,
|
|
521
|
+
});
|
|
522
|
+
const existingBindings = Array.isArray(nextCfg.bindings)
|
|
523
|
+
? nextCfg.bindings
|
|
524
|
+
: [];
|
|
525
|
+
nextCfg.bindings = existingBindings.filter((binding) => {
|
|
526
|
+
const match = binding?.match || {};
|
|
527
|
+
const hasScopedFields =
|
|
528
|
+
!!match.peer ||
|
|
529
|
+
!!match.parentPeer ||
|
|
530
|
+
!!String(match.guildId || "").trim() ||
|
|
531
|
+
!!String(match.teamId || "").trim() ||
|
|
532
|
+
(Array.isArray(match.roles) && match.roles.length > 0);
|
|
533
|
+
if (hasScopedFields) return true;
|
|
534
|
+
return !matchesBinding(match, targetMatch);
|
|
535
|
+
});
|
|
536
|
+
if (!nextChannels[provider] && nextCfg.plugins?.entries?.[provider]) {
|
|
537
|
+
nextCfg.plugins.entries[provider] = {
|
|
538
|
+
...(nextCfg.plugins.entries[provider] || {}),
|
|
539
|
+
enabled: false,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
saveConfig({ fsImpl, OPENCLAW_DIR, config: nextCfg });
|
|
543
|
+
|
|
544
|
+
const envKey = deriveChannelEnvKey({ provider, accountId });
|
|
545
|
+
const currentEnvVars = readEnvFile();
|
|
546
|
+
const previousEnvVars = Array.isArray(currentEnvVars)
|
|
547
|
+
? currentEnvVars
|
|
548
|
+
: [];
|
|
549
|
+
const nextEnvVars = previousEnvVars.filter(
|
|
550
|
+
(entry) => String(entry?.key || "").trim() !== envKey,
|
|
551
|
+
);
|
|
552
|
+
if (nextEnvVars.length !== previousEnvVars.length) {
|
|
553
|
+
writeEnvFile(nextEnvVars);
|
|
554
|
+
reloadEnv();
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
cleanupChannelAccountPairingFiles({ provider, accountId });
|
|
558
|
+
return { ok: true };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const removeArgs = [
|
|
562
|
+
"channels remove",
|
|
563
|
+
`--channel ${shellEscapeArg(provider)}`,
|
|
564
|
+
`--account ${shellEscapeArg(accountId)}`,
|
|
565
|
+
"--delete",
|
|
566
|
+
].filter(Boolean);
|
|
567
|
+
const removeResult = await clawCmd(removeArgs.join(" "), {
|
|
568
|
+
quiet: true,
|
|
569
|
+
timeoutMs: 30000,
|
|
570
|
+
});
|
|
571
|
+
if (!removeResult?.ok) {
|
|
572
|
+
throw new Error(
|
|
573
|
+
removeResult?.stderr ||
|
|
574
|
+
removeResult?.stdout ||
|
|
575
|
+
"Could not delete channel account",
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const envKey = deriveChannelEnvKey({ provider, accountId });
|
|
580
|
+
const currentEnvVars = readEnvFile();
|
|
581
|
+
const previousEnvVars = Array.isArray(currentEnvVars) ? currentEnvVars : [];
|
|
582
|
+
const nextEnvVars = previousEnvVars.filter(
|
|
583
|
+
(entry) => String(entry?.key || "").trim() !== envKey,
|
|
584
|
+
);
|
|
585
|
+
if (nextEnvVars.length !== previousEnvVars.length) {
|
|
586
|
+
writeEnvFile(nextEnvVars);
|
|
587
|
+
reloadEnv();
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const nextCfg = withNormalizedAgentsConfig({
|
|
591
|
+
OPENCLAW_DIR,
|
|
592
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
593
|
+
});
|
|
594
|
+
const nextChannels =
|
|
595
|
+
nextCfg.channels && typeof nextCfg.channels === "object"
|
|
596
|
+
? { ...nextCfg.channels }
|
|
597
|
+
: {};
|
|
598
|
+
const nextProviderConfig = normalizeChannelConfig({
|
|
599
|
+
provider,
|
|
600
|
+
channelConfig:
|
|
601
|
+
nextChannels[provider] && typeof nextChannels[provider] === "object"
|
|
602
|
+
? nextChannels[provider]
|
|
603
|
+
: {},
|
|
604
|
+
});
|
|
605
|
+
const nextAccounts =
|
|
606
|
+
nextProviderConfig.accounts &&
|
|
607
|
+
typeof nextProviderConfig.accounts === "object"
|
|
608
|
+
? { ...nextProviderConfig.accounts }
|
|
609
|
+
: {};
|
|
610
|
+
delete nextAccounts[accountId];
|
|
611
|
+
if (Object.keys(nextAccounts).length > 0) {
|
|
612
|
+
nextProviderConfig.accounts = nextAccounts;
|
|
613
|
+
nextChannels[provider] = nextProviderConfig;
|
|
614
|
+
} else {
|
|
615
|
+
delete nextChannels[provider];
|
|
616
|
+
}
|
|
617
|
+
nextCfg.channels = nextChannels;
|
|
618
|
+
const targetMatch = normalizeBindingMatch({ channel: provider, accountId });
|
|
619
|
+
const existingBindings = Array.isArray(nextCfg.bindings)
|
|
620
|
+
? nextCfg.bindings
|
|
621
|
+
: [];
|
|
622
|
+
nextCfg.bindings = existingBindings.filter((binding) => {
|
|
623
|
+
const match = binding?.match || {};
|
|
624
|
+
const hasScopedFields =
|
|
625
|
+
!!match.peer ||
|
|
626
|
+
!!match.parentPeer ||
|
|
627
|
+
!!String(match.guildId || "").trim() ||
|
|
628
|
+
!!String(match.teamId || "").trim() ||
|
|
629
|
+
(Array.isArray(match.roles) && match.roles.length > 0);
|
|
630
|
+
if (hasScopedFields) return true;
|
|
631
|
+
return !matchesBinding(match, targetMatch);
|
|
632
|
+
});
|
|
633
|
+
saveConfig({ fsImpl, OPENCLAW_DIR, config: nextCfg });
|
|
634
|
+
|
|
635
|
+
cleanupChannelAccountPairingFiles({ provider, accountId });
|
|
636
|
+
return { ok: true };
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
const listConfiguredChannelAccountsWithMaskedTokens = () => {
|
|
640
|
+
const channels = listConfiguredChannelAccounts({
|
|
641
|
+
fsImpl,
|
|
642
|
+
OPENCLAW_DIR,
|
|
643
|
+
cfg: withNormalizedAgentsConfig({
|
|
644
|
+
OPENCLAW_DIR,
|
|
645
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
646
|
+
}),
|
|
647
|
+
});
|
|
648
|
+
const envVars = readEnvFile();
|
|
649
|
+
const envKeySet = new Set(
|
|
650
|
+
(Array.isArray(envVars) ? envVars : [])
|
|
651
|
+
.filter((v) => v?.key && String(v?.value || "").trim())
|
|
652
|
+
.map((v) => String(v.key).trim()),
|
|
653
|
+
);
|
|
654
|
+
return channels.map((entry) => ({
|
|
655
|
+
...entry,
|
|
656
|
+
accounts: entry.accounts.map((account) => ({
|
|
657
|
+
...account,
|
|
658
|
+
token: envKeySet.has(String(account.envKey || "").trim())
|
|
659
|
+
? kMaskedChannelToken
|
|
660
|
+
: "",
|
|
661
|
+
})),
|
|
662
|
+
}));
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
return {
|
|
666
|
+
getChannelAccountToken,
|
|
667
|
+
createChannelAccount,
|
|
668
|
+
updateChannelAccount,
|
|
669
|
+
deleteChannelAccount,
|
|
670
|
+
listConfiguredChannelAccountsWithMaskedTokens,
|
|
671
|
+
};
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
module.exports = { createChannelsDomain };
|