@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,631 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const {
|
|
3
|
+
readOpenclawConfig,
|
|
4
|
+
writeOpenclawConfig,
|
|
5
|
+
} = require("../openclaw-config");
|
|
6
|
+
|
|
7
|
+
const kDefaultAgentId = "main";
|
|
8
|
+
const kAgentIdPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
9
|
+
const kChannelAccountIdPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
10
|
+
const kDefaultWorkspaceBasename = "workspace";
|
|
11
|
+
const kWorkspaceFolderPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
12
|
+
const kDefaultAgentFiles = ["SOUL.md", "AGENTS.md", "USER.md", "IDENTITY.md"];
|
|
13
|
+
const kChannelEnvKeys = {
|
|
14
|
+
telegram: "TELEGRAM_BOT_TOKEN",
|
|
15
|
+
discord: "DISCORD_BOT_TOKEN",
|
|
16
|
+
};
|
|
17
|
+
const kChannelTokenFields = {
|
|
18
|
+
telegram: "botToken",
|
|
19
|
+
discord: "token",
|
|
20
|
+
};
|
|
21
|
+
const kChannelLabels = {
|
|
22
|
+
telegram: "Telegram",
|
|
23
|
+
discord: "Discord",
|
|
24
|
+
};
|
|
25
|
+
const kMaskedChannelToken = "********";
|
|
26
|
+
|
|
27
|
+
const shellEscapeArg = (value) =>
|
|
28
|
+
`'${String(value || "").replace(/'/g, `'\\''`)}'`;
|
|
29
|
+
|
|
30
|
+
const resolveCredentialsDirPath = ({ OPENCLAW_DIR }) =>
|
|
31
|
+
path.join(OPENCLAW_DIR, "credentials");
|
|
32
|
+
|
|
33
|
+
const resolveAgentWorkspacePath = ({ OPENCLAW_DIR, agentId }) =>
|
|
34
|
+
path.join(
|
|
35
|
+
OPENCLAW_DIR,
|
|
36
|
+
agentId === kDefaultAgentId
|
|
37
|
+
? kDefaultWorkspaceBasename
|
|
38
|
+
: `${kDefaultWorkspaceBasename}-${agentId}`,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const resolveAgentDirPath = ({ OPENCLAW_DIR, agentId }) =>
|
|
42
|
+
path.join(OPENCLAW_DIR, "agents", agentId, "agent");
|
|
43
|
+
|
|
44
|
+
const loadConfig = ({ fsImpl, OPENCLAW_DIR }) =>
|
|
45
|
+
readOpenclawConfig({
|
|
46
|
+
fsModule: fsImpl,
|
|
47
|
+
openclawDir: OPENCLAW_DIR,
|
|
48
|
+
fallback: {},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const saveConfig = ({ fsImpl, OPENCLAW_DIR, config }) => {
|
|
52
|
+
writeOpenclawConfig({
|
|
53
|
+
fsModule: fsImpl,
|
|
54
|
+
openclawDir: OPENCLAW_DIR,
|
|
55
|
+
config,
|
|
56
|
+
spacing: 2,
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const ensurePluginAllowed = ({ cfg, pluginKey }) => {
|
|
61
|
+
if (!cfg.plugins || typeof cfg.plugins !== "object") cfg.plugins = {};
|
|
62
|
+
if (!Array.isArray(cfg.plugins.allow)) cfg.plugins.allow = [];
|
|
63
|
+
if (!cfg.plugins.entries || typeof cfg.plugins.entries !== "object") {
|
|
64
|
+
cfg.plugins.entries = {};
|
|
65
|
+
}
|
|
66
|
+
if (!cfg.plugins.allow.includes(pluginKey)) {
|
|
67
|
+
cfg.plugins.allow.push(pluginKey);
|
|
68
|
+
}
|
|
69
|
+
cfg.plugins.entries[pluginKey] = {
|
|
70
|
+
...(cfg.plugins.entries[pluginKey] &&
|
|
71
|
+
typeof cfg.plugins.entries[pluginKey] === "object"
|
|
72
|
+
? cfg.plugins.entries[pluginKey]
|
|
73
|
+
: {}),
|
|
74
|
+
enabled: true,
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const normalizeAgentsList = ({ list }) =>
|
|
79
|
+
(Array.isArray(list) ? list : [])
|
|
80
|
+
.filter((entry) => entry && typeof entry === "object")
|
|
81
|
+
.map((entry) => ({ ...entry }));
|
|
82
|
+
|
|
83
|
+
const normalizeAgentDefaults = ({ cfg }) => ({
|
|
84
|
+
model: cfg?.agents?.defaults?.model || {},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const cloneJson = (value) => JSON.parse(JSON.stringify(value));
|
|
88
|
+
const isEnvRef = (value) =>
|
|
89
|
+
/^\$\{[A-Z_][A-Z0-9_]*\}$/.test(String(value || "").trim());
|
|
90
|
+
|
|
91
|
+
const normalizePeerMatch = (value) => {
|
|
92
|
+
if (!value || typeof value !== "object") return undefined;
|
|
93
|
+
const kind = String(value.kind || "").trim();
|
|
94
|
+
const id = String(value.id || "").trim();
|
|
95
|
+
if (!kind || !id) return undefined;
|
|
96
|
+
return { kind, id };
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const normalizeBindingMatch = (input = {}) => {
|
|
100
|
+
const channel = String(input.channel || "").trim();
|
|
101
|
+
if (!channel) {
|
|
102
|
+
throw new Error("Binding channel is required");
|
|
103
|
+
}
|
|
104
|
+
const accountId = String(input.accountId || "").trim();
|
|
105
|
+
const guildId = String(input.guildId || "").trim();
|
|
106
|
+
const teamId = String(input.teamId || "").trim();
|
|
107
|
+
const peer = normalizePeerMatch(input.peer);
|
|
108
|
+
const parentPeer = normalizePeerMatch(input.parentPeer);
|
|
109
|
+
const roles = Array.isArray(input.roles)
|
|
110
|
+
? input.roles.map((entry) => String(entry || "").trim()).filter(Boolean)
|
|
111
|
+
: [];
|
|
112
|
+
return {
|
|
113
|
+
channel,
|
|
114
|
+
...(accountId ? { accountId } : {}),
|
|
115
|
+
...(guildId ? { guildId } : {}),
|
|
116
|
+
...(teamId ? { teamId } : {}),
|
|
117
|
+
...(peer ? { peer } : {}),
|
|
118
|
+
...(parentPeer ? { parentPeer } : {}),
|
|
119
|
+
...(roles.length > 0 ? { roles } : {}),
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const toComparableBindingMatch = (input = {}) => {
|
|
124
|
+
const match = normalizeBindingMatch(input);
|
|
125
|
+
return {
|
|
126
|
+
...match,
|
|
127
|
+
...(match.accountId ? {} : { accountId: "default" }),
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const matchesBinding = (left, right) =>
|
|
132
|
+
JSON.stringify(toComparableBindingMatch(left)) ===
|
|
133
|
+
JSON.stringify(toComparableBindingMatch(right));
|
|
134
|
+
|
|
135
|
+
const isValidChannelAccountId = (value) =>
|
|
136
|
+
kChannelAccountIdPattern.test(String(value || "").trim());
|
|
137
|
+
|
|
138
|
+
const normalizeChannelProvider = (value) => {
|
|
139
|
+
const provider = String(value || "")
|
|
140
|
+
.trim()
|
|
141
|
+
.toLowerCase();
|
|
142
|
+
if (!provider || !kChannelEnvKeys[provider]) {
|
|
143
|
+
throw new Error("Unsupported channel provider");
|
|
144
|
+
}
|
|
145
|
+
return provider;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const deriveChannelEnvKey = ({ provider, accountId }) => {
|
|
149
|
+
const envKey = kChannelEnvKeys[normalizeChannelProvider(provider)];
|
|
150
|
+
const normalizedAccountId = String(accountId || "").trim();
|
|
151
|
+
if (!normalizedAccountId || normalizedAccountId === "default") return envKey;
|
|
152
|
+
return `${envKey}_${normalizedAccountId.replace(/-/g, "_").toUpperCase()}`;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const getConfiguredChannelEnvKeys = (cfg) => {
|
|
156
|
+
const keys = new Set();
|
|
157
|
+
const channels =
|
|
158
|
+
cfg?.channels && typeof cfg.channels === "object" ? cfg.channels : {};
|
|
159
|
+
for (const [provider, providerConfig] of Object.entries(channels)) {
|
|
160
|
+
if (!kChannelEnvKeys[provider]) continue;
|
|
161
|
+
const accounts =
|
|
162
|
+
providerConfig?.accounts && typeof providerConfig.accounts === "object"
|
|
163
|
+
? providerConfig.accounts
|
|
164
|
+
: {};
|
|
165
|
+
for (const accountId of Object.keys(accounts)) {
|
|
166
|
+
keys.add(deriveChannelEnvKey({ provider, accountId }));
|
|
167
|
+
}
|
|
168
|
+
if (Object.keys(accounts).length === 0 && providerConfig?.enabled) {
|
|
169
|
+
keys.add(kChannelEnvKeys[provider]);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return keys;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const assertActiveChannelTokenEnvVars = ({ cfg, envVars }) => {
|
|
176
|
+
const envMap = new Map(
|
|
177
|
+
(Array.isArray(envVars) ? envVars : [])
|
|
178
|
+
.map((entry) => [
|
|
179
|
+
String(entry?.key || "").trim(),
|
|
180
|
+
String(entry?.value || "").trim(),
|
|
181
|
+
])
|
|
182
|
+
.filter(([key]) => key),
|
|
183
|
+
);
|
|
184
|
+
const channels =
|
|
185
|
+
cfg?.channels && typeof cfg.channels === "object" ? cfg.channels : {};
|
|
186
|
+
for (const [provider, providerConfig] of Object.entries(channels)) {
|
|
187
|
+
if (!kChannelEnvKeys[provider]) continue;
|
|
188
|
+
if (providerConfig?.enabled === false) continue;
|
|
189
|
+
const normalizedProviderConfig = normalizeChannelConfig({
|
|
190
|
+
provider,
|
|
191
|
+
channelConfig: providerConfig,
|
|
192
|
+
});
|
|
193
|
+
const accounts =
|
|
194
|
+
normalizedProviderConfig.accounts &&
|
|
195
|
+
typeof normalizedProviderConfig.accounts === "object"
|
|
196
|
+
? normalizedProviderConfig.accounts
|
|
197
|
+
: {};
|
|
198
|
+
const accountEntries =
|
|
199
|
+
Object.keys(accounts).length > 0
|
|
200
|
+
? Object.entries(accounts)
|
|
201
|
+
: [["default", {}]];
|
|
202
|
+
for (const [accountId, accountConfig] of accountEntries) {
|
|
203
|
+
if (accountConfig?.enabled === false) continue;
|
|
204
|
+
const envKey = deriveChannelEnvKey({ provider, accountId });
|
|
205
|
+
const envValue = String(envMap.get(envKey) || "").trim();
|
|
206
|
+
if (!envValue) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`Missing required channel token env var ${envKey} for active channel ${provider}/${accountId}`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const normalizeChannelConfig = ({ provider, channelConfig }) => {
|
|
216
|
+
const normalizedProvider = normalizeChannelProvider(provider);
|
|
217
|
+
const nextConfig =
|
|
218
|
+
channelConfig && typeof channelConfig === "object"
|
|
219
|
+
? cloneJson(channelConfig)
|
|
220
|
+
: {};
|
|
221
|
+
const existingAccounts =
|
|
222
|
+
nextConfig.accounts && typeof nextConfig.accounts === "object"
|
|
223
|
+
? { ...nextConfig.accounts }
|
|
224
|
+
: {};
|
|
225
|
+
const tokenField = kChannelTokenFields[normalizedProvider];
|
|
226
|
+
if (Object.keys(existingAccounts).length > 0) {
|
|
227
|
+
if (tokenField) {
|
|
228
|
+
for (const [accountId, accountConfig] of Object.entries(
|
|
229
|
+
existingAccounts,
|
|
230
|
+
)) {
|
|
231
|
+
if (!accountConfig || typeof accountConfig !== "object") continue;
|
|
232
|
+
const nextAccountConfig = { ...accountConfig };
|
|
233
|
+
const rawTokenFieldValue = String(
|
|
234
|
+
nextAccountConfig[tokenField] || "",
|
|
235
|
+
).trim();
|
|
236
|
+
if (rawTokenFieldValue && !isEnvRef(rawTokenFieldValue)) {
|
|
237
|
+
nextAccountConfig[tokenField] = `\${${deriveChannelEnvKey({
|
|
238
|
+
provider: normalizedProvider,
|
|
239
|
+
accountId,
|
|
240
|
+
})}}`;
|
|
241
|
+
}
|
|
242
|
+
existingAccounts[accountId] = nextAccountConfig;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
nextConfig.accounts = existingAccounts;
|
|
246
|
+
return nextConfig;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const defaultAccountConfig = {};
|
|
250
|
+
for (const [key, value] of Object.entries(nextConfig)) {
|
|
251
|
+
if (key === "enabled" || key === "accounts" || key === "defaultAccount")
|
|
252
|
+
continue;
|
|
253
|
+
defaultAccountConfig[key] = cloneJson(value);
|
|
254
|
+
delete nextConfig[key];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const defaultTokenEnvRef = `\${${deriveChannelEnvKey({
|
|
258
|
+
provider: normalizedProvider,
|
|
259
|
+
accountId: "default",
|
|
260
|
+
})}}`;
|
|
261
|
+
if (tokenField && defaultAccountConfig[tokenField]) {
|
|
262
|
+
const rawTokenFieldValue = String(
|
|
263
|
+
defaultAccountConfig[tokenField] || "",
|
|
264
|
+
).trim();
|
|
265
|
+
if (rawTokenFieldValue && !isEnvRef(rawTokenFieldValue)) {
|
|
266
|
+
defaultAccountConfig[tokenField] = defaultTokenEnvRef;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (
|
|
270
|
+
Object.keys(defaultAccountConfig).length > 0 ||
|
|
271
|
+
defaultAccountConfig[tokenField]
|
|
272
|
+
) {
|
|
273
|
+
nextConfig.accounts = { default: defaultAccountConfig };
|
|
274
|
+
if (!String(nextConfig.defaultAccount || "").trim()) {
|
|
275
|
+
nextConfig.defaultAccount = "default";
|
|
276
|
+
}
|
|
277
|
+
} else {
|
|
278
|
+
nextConfig.accounts = {};
|
|
279
|
+
}
|
|
280
|
+
return nextConfig;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const appendBindingToConfig = ({ cfg, agentId, match }) => {
|
|
284
|
+
const normalizedAgentId = String(agentId || "").trim();
|
|
285
|
+
const existingBindings = Array.isArray(cfg.bindings) ? cfg.bindings : [];
|
|
286
|
+
const conflictingBinding = existingBindings.find((binding) =>
|
|
287
|
+
matchesBinding(binding?.match || {}, match),
|
|
288
|
+
);
|
|
289
|
+
if (conflictingBinding) {
|
|
290
|
+
const conflictingAgentId = String(conflictingBinding.agentId || "").trim();
|
|
291
|
+
if (conflictingAgentId === normalizedAgentId) {
|
|
292
|
+
return cloneJson(conflictingBinding);
|
|
293
|
+
}
|
|
294
|
+
throw new Error(
|
|
295
|
+
`Binding already assigned to agent "${conflictingAgentId}"`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
const nextBinding = {
|
|
299
|
+
agentId: normalizedAgentId,
|
|
300
|
+
match,
|
|
301
|
+
};
|
|
302
|
+
cfg.bindings = [...existingBindings, nextBinding];
|
|
303
|
+
return cloneJson(nextBinding);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const buildBindingSpec = ({ provider, accountId }) => {
|
|
307
|
+
const channel = normalizeChannelProvider(provider);
|
|
308
|
+
const normalizedAccountId = String(accountId || "").trim();
|
|
309
|
+
return normalizedAccountId ? `${channel}:${normalizedAccountId}` : channel;
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const hasLegacyDefaultChannelAccount = ({ config }) =>
|
|
313
|
+
Object.keys(config || {}).some(
|
|
314
|
+
(entry) =>
|
|
315
|
+
entry !== "accounts" && entry !== "defaultAccount" && entry !== "enabled",
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
const normalizeChannelAccountId = (value) =>
|
|
319
|
+
String(value || "").trim() || "default";
|
|
320
|
+
|
|
321
|
+
const resolveCredentialPairingAccountId = ({ channelId, fileName }) => {
|
|
322
|
+
const prefix = `${String(channelId || "").trim()}-`;
|
|
323
|
+
const suffix = "-allowFrom.json";
|
|
324
|
+
const rawFileName = String(fileName || "").trim();
|
|
325
|
+
if (!rawFileName.startsWith(prefix) || !rawFileName.endsWith(suffix)) {
|
|
326
|
+
return "";
|
|
327
|
+
}
|
|
328
|
+
const rawAccountId = rawFileName.slice(prefix.length, -suffix.length);
|
|
329
|
+
return normalizeChannelAccountId(rawAccountId);
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const readPairedCountsByAccount = ({
|
|
333
|
+
fsImpl,
|
|
334
|
+
OPENCLAW_DIR,
|
|
335
|
+
channelId,
|
|
336
|
+
accountIds,
|
|
337
|
+
config,
|
|
338
|
+
}) => {
|
|
339
|
+
const counts = new Map(
|
|
340
|
+
(Array.isArray(accountIds) ? accountIds : []).map((accountId) => [
|
|
341
|
+
normalizeChannelAccountId(accountId),
|
|
342
|
+
0,
|
|
343
|
+
]),
|
|
344
|
+
);
|
|
345
|
+
const credentialsDir = resolveCredentialsDirPath({ OPENCLAW_DIR });
|
|
346
|
+
try {
|
|
347
|
+
const files = fsImpl
|
|
348
|
+
.readdirSync(credentialsDir)
|
|
349
|
+
.filter(
|
|
350
|
+
(fileName) =>
|
|
351
|
+
String(fileName || "").startsWith(
|
|
352
|
+
`${String(channelId || "").trim()}-`,
|
|
353
|
+
) && String(fileName || "").endsWith("-allowFrom.json"),
|
|
354
|
+
);
|
|
355
|
+
for (const fileName of files) {
|
|
356
|
+
const accountId = resolveCredentialPairingAccountId({
|
|
357
|
+
channelId,
|
|
358
|
+
fileName,
|
|
359
|
+
});
|
|
360
|
+
if (!accountId || !counts.has(accountId)) continue;
|
|
361
|
+
const filePath = path.join(credentialsDir, fileName);
|
|
362
|
+
const parsed = JSON.parse(fsImpl.readFileSync(filePath, "utf8"));
|
|
363
|
+
const pairedCount = Array.isArray(parsed?.allowFrom)
|
|
364
|
+
? parsed.allowFrom.length
|
|
365
|
+
: 0;
|
|
366
|
+
counts.set(accountId, Number(counts.get(accountId) || 0) + pairedCount);
|
|
367
|
+
}
|
|
368
|
+
} catch {}
|
|
369
|
+
|
|
370
|
+
for (const accountId of counts.keys()) {
|
|
371
|
+
const accountConfig =
|
|
372
|
+
accountId === "default" &&
|
|
373
|
+
!(config.accounts && typeof config.accounts === "object")
|
|
374
|
+
? config
|
|
375
|
+
: config.accounts?.[accountId] || {};
|
|
376
|
+
const inlineAllowFrom = accountConfig?.allowFrom;
|
|
377
|
+
if (!Array.isArray(inlineAllowFrom)) continue;
|
|
378
|
+
counts.set(
|
|
379
|
+
accountId,
|
|
380
|
+
Number(counts.get(accountId) || 0) + inlineAllowFrom.length,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return counts;
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const listConfiguredChannelAccounts = ({ fsImpl, OPENCLAW_DIR, cfg }) => {
|
|
388
|
+
const bindings = Array.isArray(cfg?.bindings) ? cfg.bindings : [];
|
|
389
|
+
const boundAccountMap = new Map();
|
|
390
|
+
for (const binding of bindings) {
|
|
391
|
+
const match = binding?.match || {};
|
|
392
|
+
const hasScopedFields =
|
|
393
|
+
!!match.peer ||
|
|
394
|
+
!!match.parentPeer ||
|
|
395
|
+
!!String(match.guildId || "").trim() ||
|
|
396
|
+
!!String(match.teamId || "").trim() ||
|
|
397
|
+
(Array.isArray(match.roles) && match.roles.length > 0);
|
|
398
|
+
if (hasScopedFields) continue;
|
|
399
|
+
const channel = String(match.channel || "").trim();
|
|
400
|
+
if (!channel) continue;
|
|
401
|
+
const accountId = String(match.accountId || "").trim() || "default";
|
|
402
|
+
const agentId = String(binding?.agentId || "").trim();
|
|
403
|
+
if (!agentId) continue;
|
|
404
|
+
const key = `${channel}:${accountId}`;
|
|
405
|
+
if (!boundAccountMap.has(key)) {
|
|
406
|
+
boundAccountMap.set(key, agentId);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const channels =
|
|
410
|
+
cfg?.channels && typeof cfg.channels === "object" ? cfg.channels : {};
|
|
411
|
+
return Object.entries(channels)
|
|
412
|
+
.map(([channelId, channelConfig]) => {
|
|
413
|
+
if (!kChannelEnvKeys[String(channelId || "").trim()]) return null;
|
|
414
|
+
const config =
|
|
415
|
+
channelConfig && typeof channelConfig === "object" ? channelConfig : {};
|
|
416
|
+
const accountsConfig =
|
|
417
|
+
config.accounts && typeof config.accounts === "object"
|
|
418
|
+
? config.accounts
|
|
419
|
+
: {};
|
|
420
|
+
const accountIds = Object.keys(accountsConfig)
|
|
421
|
+
.map((entry) => String(entry || "").trim())
|
|
422
|
+
.filter(Boolean);
|
|
423
|
+
const topLevelKeys = Object.keys(config).filter(
|
|
424
|
+
(entry) =>
|
|
425
|
+
entry !== "accounts" &&
|
|
426
|
+
entry !== "defaultAccount" &&
|
|
427
|
+
entry !== "enabled",
|
|
428
|
+
);
|
|
429
|
+
if (accountIds.length === 0 && topLevelKeys.length === 0) return null;
|
|
430
|
+
const normalizedAccountIds = accountIds.includes("default")
|
|
431
|
+
? accountIds
|
|
432
|
+
: topLevelKeys.length > 0
|
|
433
|
+
? ["default", ...accountIds]
|
|
434
|
+
: accountIds;
|
|
435
|
+
const pairedCounts = readPairedCountsByAccount({
|
|
436
|
+
fsImpl,
|
|
437
|
+
OPENCLAW_DIR,
|
|
438
|
+
channelId,
|
|
439
|
+
accountIds: normalizedAccountIds,
|
|
440
|
+
config,
|
|
441
|
+
});
|
|
442
|
+
return {
|
|
443
|
+
channel: String(channelId || "").trim(),
|
|
444
|
+
accounts: normalizedAccountIds
|
|
445
|
+
.map((accountId) => {
|
|
446
|
+
const accountConfig =
|
|
447
|
+
accountId === "default" && accountIds.length === 0
|
|
448
|
+
? config
|
|
449
|
+
: accountsConfig?.[accountId] || {};
|
|
450
|
+
return {
|
|
451
|
+
id: accountId,
|
|
452
|
+
name: String(accountConfig?.name || "").trim(),
|
|
453
|
+
envKey: deriveChannelEnvKey({ provider: channelId, accountId }),
|
|
454
|
+
boundAgentId:
|
|
455
|
+
boundAccountMap.get(
|
|
456
|
+
`${String(channelId || "").trim()}:${accountId}`,
|
|
457
|
+
) || "",
|
|
458
|
+
paired: Number(pairedCounts.get(accountId) || 0),
|
|
459
|
+
status:
|
|
460
|
+
Number(pairedCounts.get(accountId) || 0) > 0
|
|
461
|
+
? "paired"
|
|
462
|
+
: "configured",
|
|
463
|
+
};
|
|
464
|
+
}),
|
|
465
|
+
};
|
|
466
|
+
})
|
|
467
|
+
.filter(Boolean);
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const getSafeStat = ({ fsImpl, targetPath }) => {
|
|
471
|
+
try {
|
|
472
|
+
if (typeof fsImpl.lstatSync === "function") {
|
|
473
|
+
return fsImpl.lstatSync(targetPath);
|
|
474
|
+
}
|
|
475
|
+
if (typeof fsImpl.statSync === "function") {
|
|
476
|
+
return fsImpl.statSync(targetPath);
|
|
477
|
+
}
|
|
478
|
+
} catch {}
|
|
479
|
+
return null;
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const calculatePathSizeBytes = ({ fsImpl, targetPath }) => {
|
|
483
|
+
const stat = getSafeStat({ fsImpl, targetPath });
|
|
484
|
+
if (!stat) return 0;
|
|
485
|
+
if (typeof stat.isSymbolicLink === "function" && stat.isSymbolicLink())
|
|
486
|
+
return 0;
|
|
487
|
+
if (typeof stat.isFile === "function" && stat.isFile()) {
|
|
488
|
+
return Number(stat.size || 0);
|
|
489
|
+
}
|
|
490
|
+
if (!(typeof stat.isDirectory === "function" && stat.isDirectory())) {
|
|
491
|
+
return 0;
|
|
492
|
+
}
|
|
493
|
+
let entries = [];
|
|
494
|
+
try {
|
|
495
|
+
entries = fsImpl.readdirSync(targetPath) || [];
|
|
496
|
+
} catch {
|
|
497
|
+
return 0;
|
|
498
|
+
}
|
|
499
|
+
return entries.reduce(
|
|
500
|
+
(total, entry) =>
|
|
501
|
+
total +
|
|
502
|
+
calculatePathSizeBytes({
|
|
503
|
+
fsImpl,
|
|
504
|
+
targetPath: path.join(targetPath, String(entry || "")),
|
|
505
|
+
}),
|
|
506
|
+
0,
|
|
507
|
+
);
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
const getImplicitMainAgent = ({ OPENCLAW_DIR, cfg }) => {
|
|
511
|
+
const defaults = normalizeAgentDefaults({ cfg });
|
|
512
|
+
const defaultPrimaryModel = String(defaults?.model?.primary || "").trim();
|
|
513
|
+
return {
|
|
514
|
+
id: kDefaultAgentId,
|
|
515
|
+
default: true,
|
|
516
|
+
name: "Main Agent",
|
|
517
|
+
workspace: resolveAgentWorkspacePath({
|
|
518
|
+
OPENCLAW_DIR,
|
|
519
|
+
agentId: kDefaultAgentId,
|
|
520
|
+
}),
|
|
521
|
+
agentDir: resolveAgentDirPath({ OPENCLAW_DIR, agentId: kDefaultAgentId }),
|
|
522
|
+
...(defaultPrimaryModel ? { model: { primary: defaultPrimaryModel } } : {}),
|
|
523
|
+
};
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const withNormalizedAgentsConfig = ({ OPENCLAW_DIR, cfg }) => {
|
|
527
|
+
const nextCfg = cfg && typeof cfg === "object" ? { ...cfg } : {};
|
|
528
|
+
const existingAgents =
|
|
529
|
+
nextCfg.agents && typeof nextCfg.agents === "object" ? nextCfg.agents : {};
|
|
530
|
+
const existingList = normalizeAgentsList({ list: existingAgents.list });
|
|
531
|
+
const hasMain = existingList.some(
|
|
532
|
+
(entry) => String(entry.id || "").trim() === kDefaultAgentId,
|
|
533
|
+
);
|
|
534
|
+
const nextList = hasMain
|
|
535
|
+
? existingList
|
|
536
|
+
: [getImplicitMainAgent({ OPENCLAW_DIR, cfg: nextCfg }), ...existingList];
|
|
537
|
+
|
|
538
|
+
let hasDefault = false;
|
|
539
|
+
const listWithSingleDefault = nextList.map((entry) => {
|
|
540
|
+
if (!entry.default) return entry;
|
|
541
|
+
if (hasDefault) return { ...entry, default: false };
|
|
542
|
+
hasDefault = true;
|
|
543
|
+
return { ...entry, default: true };
|
|
544
|
+
});
|
|
545
|
+
if (!hasDefault && listWithSingleDefault.length > 0) {
|
|
546
|
+
listWithSingleDefault[0] = { ...listWithSingleDefault[0], default: true };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
nextCfg.agents = {
|
|
550
|
+
...existingAgents,
|
|
551
|
+
list: listWithSingleDefault,
|
|
552
|
+
};
|
|
553
|
+
return nextCfg;
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
const isValidAgentId = (value) =>
|
|
557
|
+
kAgentIdPattern.test(String(value || "").trim());
|
|
558
|
+
|
|
559
|
+
const isValidWorkspaceFolder = (value) =>
|
|
560
|
+
kWorkspaceFolderPattern.test(String(value || "").trim());
|
|
561
|
+
|
|
562
|
+
const resolveRequestedWorkspacePath = ({
|
|
563
|
+
OPENCLAW_DIR,
|
|
564
|
+
agentId,
|
|
565
|
+
workspaceFolder,
|
|
566
|
+
}) => {
|
|
567
|
+
const normalizedFolder = String(workspaceFolder || "").trim();
|
|
568
|
+
if (!normalizedFolder)
|
|
569
|
+
return resolveAgentWorkspacePath({ OPENCLAW_DIR, agentId });
|
|
570
|
+
if (!isValidWorkspaceFolder(normalizedFolder)) {
|
|
571
|
+
throw new Error(
|
|
572
|
+
"Workspace folder must be lowercase letters, numbers, and hyphens only",
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
return path.join(OPENCLAW_DIR, normalizedFolder);
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
const ensureAgentScaffold = ({
|
|
579
|
+
fsImpl,
|
|
580
|
+
agentId,
|
|
581
|
+
workspacePath,
|
|
582
|
+
OPENCLAW_DIR,
|
|
583
|
+
}) => {
|
|
584
|
+
const agentDirPath = resolveAgentDirPath({ OPENCLAW_DIR, agentId });
|
|
585
|
+
fsImpl.mkdirSync(workspacePath, { recursive: true });
|
|
586
|
+
fsImpl.mkdirSync(agentDirPath, { recursive: true });
|
|
587
|
+
for (const fileName of kDefaultAgentFiles) {
|
|
588
|
+
const targetPath = path.join(workspacePath, fileName);
|
|
589
|
+
if (fsImpl.existsSync(targetPath)) continue;
|
|
590
|
+
fsImpl.writeFileSync(
|
|
591
|
+
targetPath,
|
|
592
|
+
`# ${fileName}\n\nCreated for agent "${agentId}".\n`,
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
workspacePath,
|
|
597
|
+
agentDirPath,
|
|
598
|
+
};
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
module.exports = {
|
|
602
|
+
kDefaultAgentId,
|
|
603
|
+
kChannelTokenFields,
|
|
604
|
+
kChannelLabels,
|
|
605
|
+
kMaskedChannelToken,
|
|
606
|
+
shellEscapeArg,
|
|
607
|
+
resolveCredentialsDirPath,
|
|
608
|
+
resolveAgentWorkspacePath,
|
|
609
|
+
loadConfig,
|
|
610
|
+
saveConfig,
|
|
611
|
+
ensurePluginAllowed,
|
|
612
|
+
cloneJson,
|
|
613
|
+
normalizeBindingMatch,
|
|
614
|
+
matchesBinding,
|
|
615
|
+
isValidChannelAccountId,
|
|
616
|
+
normalizeChannelProvider,
|
|
617
|
+
deriveChannelEnvKey,
|
|
618
|
+
getConfiguredChannelEnvKeys,
|
|
619
|
+
assertActiveChannelTokenEnvVars,
|
|
620
|
+
normalizeChannelConfig,
|
|
621
|
+
appendBindingToConfig,
|
|
622
|
+
buildBindingSpec,
|
|
623
|
+
hasLegacyDefaultChannelAccount,
|
|
624
|
+
listConfiguredChannelAccounts,
|
|
625
|
+
getSafeStat,
|
|
626
|
+
calculatePathSizeBytes,
|
|
627
|
+
withNormalizedAgentsConfig,
|
|
628
|
+
isValidAgentId,
|
|
629
|
+
resolveRequestedWorkspacePath,
|
|
630
|
+
ensureAgentScaffold,
|
|
631
|
+
};
|
package/lib/server/constants.js
CHANGED
|
@@ -17,7 +17,20 @@ const readOpenclawConfig = ({
|
|
|
17
17
|
}
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
const writeOpenclawConfig = ({
|
|
21
|
+
fsModule = fs,
|
|
22
|
+
openclawDir,
|
|
23
|
+
config = {},
|
|
24
|
+
spacing = 2,
|
|
25
|
+
} = {}) => {
|
|
26
|
+
const configPath = resolveOpenclawConfigPath({ openclawDir });
|
|
27
|
+
fsModule.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
28
|
+
fsModule.writeFileSync(configPath, JSON.stringify(config, null, spacing));
|
|
29
|
+
return configPath;
|
|
30
|
+
};
|
|
31
|
+
|
|
20
32
|
module.exports = {
|
|
21
33
|
resolveOpenclawConfigPath,
|
|
22
34
|
readOpenclawConfig,
|
|
35
|
+
writeOpenclawConfig,
|
|
23
36
|
};
|
|
@@ -3,6 +3,11 @@ const path = require("path");
|
|
|
3
3
|
const { OPENCLAW_DIR } = require("../constants");
|
|
4
4
|
const { buildManagedPaths } = require("../internal-files-migration");
|
|
5
5
|
const { parseJsonObjectFromNoisyOutput } = require("../utils/json");
|
|
6
|
+
const { quoteShellArg } = require("../utils/shell");
|
|
7
|
+
|
|
8
|
+
const kAllowedPairingChannels = new Set(["telegram", "discord"]);
|
|
9
|
+
const kSafePairingArgPattern = /^[\w\-:.]+$/;
|
|
10
|
+
const quoteCliArg = (value) => quoteShellArg(value, { strategy: "single" });
|
|
6
11
|
|
|
7
12
|
const resolvePairingStorePath = ({ openclawDir, channel }) =>
|
|
8
13
|
path.join(openclawDir, "credentials", `${String(channel).trim().toLowerCase()}-pairing.json`);
|
|
@@ -133,11 +138,32 @@ const registerPairingRoutes = ({ app, clawCmd, isOnboarded, fsModule = fs, openc
|
|
|
133
138
|
});
|
|
134
139
|
|
|
135
140
|
app.post("/api/pairings/:id/approve", async (req, res) => {
|
|
136
|
-
const channel = req.body
|
|
141
|
+
const channel = String(req.body?.channel || "telegram")
|
|
142
|
+
.trim()
|
|
143
|
+
.toLowerCase();
|
|
137
144
|
const accountId = String(req.body?.accountId || "").trim();
|
|
145
|
+
const pairingId = String(req.params.id || "").trim();
|
|
146
|
+
if (!kAllowedPairingChannels.has(channel)) {
|
|
147
|
+
return res.status(400).json({
|
|
148
|
+
ok: false,
|
|
149
|
+
error: `Unsupported pairing channel "${channel}"`,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (!pairingId || !kSafePairingArgPattern.test(pairingId)) {
|
|
153
|
+
return res.status(400).json({
|
|
154
|
+
ok: false,
|
|
155
|
+
error: "Invalid pairing id",
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (accountId && !kSafePairingArgPattern.test(accountId)) {
|
|
159
|
+
return res.status(400).json({
|
|
160
|
+
ok: false,
|
|
161
|
+
error: "Invalid account id",
|
|
162
|
+
});
|
|
163
|
+
}
|
|
138
164
|
const approveCmd = accountId
|
|
139
|
-
? `pairing approve --channel ${channel} --account ${accountId} ${
|
|
140
|
-
: `pairing approve ${channel} ${
|
|
165
|
+
? `pairing approve --channel ${quoteCliArg(channel)} --account ${quoteCliArg(accountId)} ${quoteCliArg(pairingId)}`
|
|
166
|
+
: `pairing approve ${quoteCliArg(channel)} ${quoteCliArg(pairingId)}`;
|
|
141
167
|
const result = await clawCmd(approveCmd);
|
|
142
168
|
res.json(result);
|
|
143
169
|
});
|