@chrysb/alphaclaw 0.5.6 → 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 +64 -26
- 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-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 +73 -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/telegram-workspace.js +61 -10
- package/lib/server/topic-registry.js +66 -7
- package/lib/server/watchdog.js +39 -1
- package/lib/server/webhooks.js +60 -12
- package/lib/server.js +21 -7
- 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
|
@@ -0,0 +1,1478 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const kDefaultAgentId = "main";
|
|
5
|
+
const kAgentIdPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
6
|
+
const kChannelAccountIdPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
7
|
+
const kDefaultWorkspaceBasename = "workspace";
|
|
8
|
+
const kWorkspaceFolderPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
9
|
+
const kDefaultAgentFiles = ["SOUL.md", "AGENTS.md", "USER.md", "IDENTITY.md"];
|
|
10
|
+
const kChannelEnvKeys = {
|
|
11
|
+
telegram: "TELEGRAM_BOT_TOKEN",
|
|
12
|
+
discord: "DISCORD_BOT_TOKEN",
|
|
13
|
+
};
|
|
14
|
+
const kChannelTokenFields = {
|
|
15
|
+
telegram: "botToken",
|
|
16
|
+
discord: "token",
|
|
17
|
+
};
|
|
18
|
+
const kChannelLabels = {
|
|
19
|
+
telegram: "Telegram",
|
|
20
|
+
discord: "Discord",
|
|
21
|
+
};
|
|
22
|
+
const kMaskedChannelToken = "********";
|
|
23
|
+
|
|
24
|
+
const shellEscapeArg = (value) =>
|
|
25
|
+
`'${String(value || "").replace(/'/g, `'\\''`)}'`;
|
|
26
|
+
|
|
27
|
+
const resolveConfigPath = ({ OPENCLAW_DIR }) =>
|
|
28
|
+
path.join(OPENCLAW_DIR, "openclaw.json");
|
|
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 parseConfig = ({ fsImpl, configPath }) => {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(fsImpl.readFileSync(configPath, "utf8"));
|
|
47
|
+
} catch {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const loadConfig = ({ fsImpl, OPENCLAW_DIR }) =>
|
|
53
|
+
parseConfig({
|
|
54
|
+
fsImpl,
|
|
55
|
+
configPath: resolveConfigPath({ OPENCLAW_DIR }),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const saveConfig = ({ fsImpl, OPENCLAW_DIR, config }) => {
|
|
59
|
+
const configPath = resolveConfigPath({ OPENCLAW_DIR });
|
|
60
|
+
fsImpl.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
61
|
+
fsImpl.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const ensurePluginAllowed = ({ cfg, pluginKey }) => {
|
|
65
|
+
if (!cfg.plugins || typeof cfg.plugins !== "object") cfg.plugins = {};
|
|
66
|
+
if (!Array.isArray(cfg.plugins.allow)) cfg.plugins.allow = [];
|
|
67
|
+
if (!cfg.plugins.entries || typeof cfg.plugins.entries !== "object") {
|
|
68
|
+
cfg.plugins.entries = {};
|
|
69
|
+
}
|
|
70
|
+
if (!cfg.plugins.allow.includes(pluginKey)) {
|
|
71
|
+
cfg.plugins.allow.push(pluginKey);
|
|
72
|
+
}
|
|
73
|
+
cfg.plugins.entries[pluginKey] = {
|
|
74
|
+
...(cfg.plugins.entries[pluginKey] &&
|
|
75
|
+
typeof cfg.plugins.entries[pluginKey] === "object"
|
|
76
|
+
? cfg.plugins.entries[pluginKey]
|
|
77
|
+
: {}),
|
|
78
|
+
enabled: true,
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const normalizeAgentsList = ({ list }) =>
|
|
83
|
+
(Array.isArray(list) ? list : [])
|
|
84
|
+
.filter((entry) => entry && typeof entry === "object")
|
|
85
|
+
.map((entry) => ({ ...entry }));
|
|
86
|
+
|
|
87
|
+
const normalizeAgentDefaults = ({ cfg }) => ({
|
|
88
|
+
model: cfg?.agents?.defaults?.model || {},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const cloneJson = (value) => JSON.parse(JSON.stringify(value));
|
|
92
|
+
const isEnvRef = (value) =>
|
|
93
|
+
/^\$\{[A-Z_][A-Z0-9_]*\}$/.test(String(value || "").trim());
|
|
94
|
+
|
|
95
|
+
const normalizePeerMatch = (value) => {
|
|
96
|
+
if (!value || typeof value !== "object") return undefined;
|
|
97
|
+
const kind = String(value.kind || "").trim();
|
|
98
|
+
const id = String(value.id || "").trim();
|
|
99
|
+
if (!kind || !id) return undefined;
|
|
100
|
+
return { kind, id };
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const normalizeBindingMatch = (input = {}) => {
|
|
104
|
+
const channel = String(input.channel || "").trim();
|
|
105
|
+
if (!channel) {
|
|
106
|
+
throw new Error("Binding channel is required");
|
|
107
|
+
}
|
|
108
|
+
const accountId = String(input.accountId || "").trim();
|
|
109
|
+
const guildId = String(input.guildId || "").trim();
|
|
110
|
+
const teamId = String(input.teamId || "").trim();
|
|
111
|
+
const peer = normalizePeerMatch(input.peer);
|
|
112
|
+
const parentPeer = normalizePeerMatch(input.parentPeer);
|
|
113
|
+
const roles = Array.isArray(input.roles)
|
|
114
|
+
? input.roles.map((entry) => String(entry || "").trim()).filter(Boolean)
|
|
115
|
+
: [];
|
|
116
|
+
return {
|
|
117
|
+
channel,
|
|
118
|
+
...(accountId ? { accountId } : {}),
|
|
119
|
+
...(guildId ? { guildId } : {}),
|
|
120
|
+
...(teamId ? { teamId } : {}),
|
|
121
|
+
...(peer ? { peer } : {}),
|
|
122
|
+
...(parentPeer ? { parentPeer } : {}),
|
|
123
|
+
...(roles.length > 0 ? { roles } : {}),
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const toComparableBindingMatch = (input = {}) => {
|
|
128
|
+
const match = normalizeBindingMatch(input);
|
|
129
|
+
return {
|
|
130
|
+
...match,
|
|
131
|
+
...(match.accountId ? {} : { accountId: "default" }),
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const matchesBinding = (left, right) =>
|
|
136
|
+
JSON.stringify(toComparableBindingMatch(left)) ===
|
|
137
|
+
JSON.stringify(toComparableBindingMatch(right));
|
|
138
|
+
|
|
139
|
+
const isValidChannelAccountId = (value) =>
|
|
140
|
+
kChannelAccountIdPattern.test(String(value || "").trim());
|
|
141
|
+
|
|
142
|
+
const normalizeChannelProvider = (value) => {
|
|
143
|
+
const provider = String(value || "")
|
|
144
|
+
.trim()
|
|
145
|
+
.toLowerCase();
|
|
146
|
+
if (!provider || !kChannelEnvKeys[provider]) {
|
|
147
|
+
throw new Error("Unsupported channel provider");
|
|
148
|
+
}
|
|
149
|
+
return provider;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const deriveChannelEnvKey = ({ provider, accountId }) => {
|
|
153
|
+
const envKey = kChannelEnvKeys[normalizeChannelProvider(provider)];
|
|
154
|
+
const normalizedAccountId = String(accountId || "").trim();
|
|
155
|
+
if (!normalizedAccountId || normalizedAccountId === "default") return envKey;
|
|
156
|
+
return `${envKey}_${normalizedAccountId.replace(/-/g, "_").toUpperCase()}`;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const getConfiguredChannelEnvKeys = (cfg) => {
|
|
160
|
+
const keys = new Set();
|
|
161
|
+
const channels =
|
|
162
|
+
cfg?.channels && typeof cfg.channels === "object" ? cfg.channels : {};
|
|
163
|
+
for (const [provider, providerConfig] of Object.entries(channels)) {
|
|
164
|
+
if (!kChannelEnvKeys[provider]) continue;
|
|
165
|
+
const accounts =
|
|
166
|
+
providerConfig?.accounts && typeof providerConfig.accounts === "object"
|
|
167
|
+
? providerConfig.accounts
|
|
168
|
+
: {};
|
|
169
|
+
for (const accountId of Object.keys(accounts)) {
|
|
170
|
+
keys.add(deriveChannelEnvKey({ provider, accountId }));
|
|
171
|
+
}
|
|
172
|
+
if (Object.keys(accounts).length === 0 && providerConfig?.enabled) {
|
|
173
|
+
keys.add(kChannelEnvKeys[provider]);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return keys;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const assertActiveChannelTokenEnvVars = ({ cfg, envVars }) => {
|
|
180
|
+
const envMap = new Map(
|
|
181
|
+
(Array.isArray(envVars) ? envVars : [])
|
|
182
|
+
.map((entry) => [
|
|
183
|
+
String(entry?.key || "").trim(),
|
|
184
|
+
String(entry?.value || "").trim(),
|
|
185
|
+
])
|
|
186
|
+
.filter(([key]) => key),
|
|
187
|
+
);
|
|
188
|
+
const channels =
|
|
189
|
+
cfg?.channels && typeof cfg.channels === "object" ? cfg.channels : {};
|
|
190
|
+
for (const [provider, providerConfig] of Object.entries(channels)) {
|
|
191
|
+
if (!kChannelEnvKeys[provider]) continue;
|
|
192
|
+
if (providerConfig?.enabled === false) continue;
|
|
193
|
+
const normalizedProviderConfig = normalizeChannelConfig({
|
|
194
|
+
provider,
|
|
195
|
+
channelConfig: providerConfig,
|
|
196
|
+
});
|
|
197
|
+
const accounts =
|
|
198
|
+
normalizedProviderConfig.accounts &&
|
|
199
|
+
typeof normalizedProviderConfig.accounts === "object"
|
|
200
|
+
? normalizedProviderConfig.accounts
|
|
201
|
+
: {};
|
|
202
|
+
const accountEntries =
|
|
203
|
+
Object.keys(accounts).length > 0
|
|
204
|
+
? Object.entries(accounts)
|
|
205
|
+
: [["default", {}]];
|
|
206
|
+
for (const [accountId, accountConfig] of accountEntries) {
|
|
207
|
+
if (accountConfig?.enabled === false) continue;
|
|
208
|
+
const envKey = deriveChannelEnvKey({ provider, accountId });
|
|
209
|
+
const envValue = String(envMap.get(envKey) || "").trim();
|
|
210
|
+
if (!envValue) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`Missing required channel token env var ${envKey} for active channel ${provider}/${accountId}`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const normalizeChannelConfig = ({ provider, channelConfig }) => {
|
|
220
|
+
const normalizedProvider = normalizeChannelProvider(provider);
|
|
221
|
+
const nextConfig =
|
|
222
|
+
channelConfig && typeof channelConfig === "object"
|
|
223
|
+
? cloneJson(channelConfig)
|
|
224
|
+
: {};
|
|
225
|
+
const existingAccounts =
|
|
226
|
+
nextConfig.accounts && typeof nextConfig.accounts === "object"
|
|
227
|
+
? { ...nextConfig.accounts }
|
|
228
|
+
: {};
|
|
229
|
+
const tokenField = kChannelTokenFields[normalizedProvider];
|
|
230
|
+
if (Object.keys(existingAccounts).length > 0) {
|
|
231
|
+
if (tokenField) {
|
|
232
|
+
for (const [accountId, accountConfig] of Object.entries(
|
|
233
|
+
existingAccounts,
|
|
234
|
+
)) {
|
|
235
|
+
if (!accountConfig || typeof accountConfig !== "object") continue;
|
|
236
|
+
const nextAccountConfig = { ...accountConfig };
|
|
237
|
+
const rawTokenFieldValue = String(
|
|
238
|
+
nextAccountConfig[tokenField] || "",
|
|
239
|
+
).trim();
|
|
240
|
+
if (rawTokenFieldValue && !isEnvRef(rawTokenFieldValue)) {
|
|
241
|
+
nextAccountConfig[tokenField] = `\${${deriveChannelEnvKey({
|
|
242
|
+
provider: normalizedProvider,
|
|
243
|
+
accountId,
|
|
244
|
+
})}}`;
|
|
245
|
+
}
|
|
246
|
+
existingAccounts[accountId] = nextAccountConfig;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
nextConfig.accounts = existingAccounts;
|
|
250
|
+
return nextConfig;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const defaultAccountConfig = {};
|
|
254
|
+
for (const [key, value] of Object.entries(nextConfig)) {
|
|
255
|
+
if (key === "enabled" || key === "accounts" || key === "defaultAccount")
|
|
256
|
+
continue;
|
|
257
|
+
defaultAccountConfig[key] = cloneJson(value);
|
|
258
|
+
delete nextConfig[key];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const defaultTokenEnvRef = `\${${deriveChannelEnvKey({
|
|
262
|
+
provider: normalizedProvider,
|
|
263
|
+
accountId: "default",
|
|
264
|
+
})}}`;
|
|
265
|
+
if (tokenField && defaultAccountConfig[tokenField]) {
|
|
266
|
+
const rawTokenFieldValue = String(
|
|
267
|
+
defaultAccountConfig[tokenField] || "",
|
|
268
|
+
).trim();
|
|
269
|
+
if (rawTokenFieldValue && !isEnvRef(rawTokenFieldValue)) {
|
|
270
|
+
defaultAccountConfig[tokenField] = defaultTokenEnvRef;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (
|
|
274
|
+
Object.keys(defaultAccountConfig).length > 0 ||
|
|
275
|
+
defaultAccountConfig[tokenField]
|
|
276
|
+
) {
|
|
277
|
+
nextConfig.accounts = { default: defaultAccountConfig };
|
|
278
|
+
if (!String(nextConfig.defaultAccount || "").trim()) {
|
|
279
|
+
nextConfig.defaultAccount = "default";
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
nextConfig.accounts = {};
|
|
283
|
+
}
|
|
284
|
+
return nextConfig;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const appendBindingToConfig = ({ cfg, agentId, match }) => {
|
|
288
|
+
const normalizedAgentId = String(agentId || "").trim();
|
|
289
|
+
const existingBindings = Array.isArray(cfg.bindings) ? cfg.bindings : [];
|
|
290
|
+
const conflictingBinding = existingBindings.find((binding) =>
|
|
291
|
+
matchesBinding(binding?.match || {}, match),
|
|
292
|
+
);
|
|
293
|
+
if (conflictingBinding) {
|
|
294
|
+
const conflictingAgentId = String(conflictingBinding.agentId || "").trim();
|
|
295
|
+
if (conflictingAgentId === normalizedAgentId) {
|
|
296
|
+
return cloneJson(conflictingBinding);
|
|
297
|
+
}
|
|
298
|
+
throw new Error(
|
|
299
|
+
`Binding already assigned to agent "${conflictingAgentId}"`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
const nextBinding = {
|
|
303
|
+
agentId: normalizedAgentId,
|
|
304
|
+
match,
|
|
305
|
+
};
|
|
306
|
+
cfg.bindings = [...existingBindings, nextBinding];
|
|
307
|
+
return cloneJson(nextBinding);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const buildBindingSpec = ({ provider, accountId }) => {
|
|
311
|
+
const channel = normalizeChannelProvider(provider);
|
|
312
|
+
const normalizedAccountId = String(accountId || "").trim();
|
|
313
|
+
return normalizedAccountId ? `${channel}:${normalizedAccountId}` : channel;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const hasLegacyDefaultChannelAccount = ({ config }) =>
|
|
317
|
+
Object.keys(config || {}).some(
|
|
318
|
+
(entry) =>
|
|
319
|
+
entry !== "accounts" && entry !== "defaultAccount" && entry !== "enabled",
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
const normalizeChannelAccountId = (value) =>
|
|
323
|
+
String(value || "").trim() || "default";
|
|
324
|
+
|
|
325
|
+
const resolveCredentialPairingAccountId = ({ channelId, fileName }) => {
|
|
326
|
+
const prefix = `${String(channelId || "").trim()}-`;
|
|
327
|
+
const suffix = "-allowFrom.json";
|
|
328
|
+
const rawFileName = String(fileName || "").trim();
|
|
329
|
+
if (!rawFileName.startsWith(prefix) || !rawFileName.endsWith(suffix)) {
|
|
330
|
+
return "";
|
|
331
|
+
}
|
|
332
|
+
const rawAccountId = rawFileName.slice(prefix.length, -suffix.length);
|
|
333
|
+
return normalizeChannelAccountId(rawAccountId);
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const readPairedCountsByAccount = ({
|
|
337
|
+
fsImpl,
|
|
338
|
+
OPENCLAW_DIR,
|
|
339
|
+
channelId,
|
|
340
|
+
accountIds,
|
|
341
|
+
config,
|
|
342
|
+
}) => {
|
|
343
|
+
const counts = new Map(
|
|
344
|
+
(Array.isArray(accountIds) ? accountIds : []).map((accountId) => [
|
|
345
|
+
normalizeChannelAccountId(accountId),
|
|
346
|
+
0,
|
|
347
|
+
]),
|
|
348
|
+
);
|
|
349
|
+
const credentialsDir = resolveCredentialsDirPath({ OPENCLAW_DIR });
|
|
350
|
+
try {
|
|
351
|
+
const files = fsImpl
|
|
352
|
+
.readdirSync(credentialsDir)
|
|
353
|
+
.filter(
|
|
354
|
+
(fileName) =>
|
|
355
|
+
String(fileName || "").startsWith(
|
|
356
|
+
`${String(channelId || "").trim()}-`,
|
|
357
|
+
) && String(fileName || "").endsWith("-allowFrom.json"),
|
|
358
|
+
);
|
|
359
|
+
for (const fileName of files) {
|
|
360
|
+
const accountId = resolveCredentialPairingAccountId({
|
|
361
|
+
channelId,
|
|
362
|
+
fileName,
|
|
363
|
+
});
|
|
364
|
+
if (!accountId || !counts.has(accountId)) continue;
|
|
365
|
+
const filePath = path.join(credentialsDir, fileName);
|
|
366
|
+
const parsed = JSON.parse(fsImpl.readFileSync(filePath, "utf8"));
|
|
367
|
+
const pairedCount = Array.isArray(parsed?.allowFrom)
|
|
368
|
+
? parsed.allowFrom.length
|
|
369
|
+
: 0;
|
|
370
|
+
counts.set(accountId, Number(counts.get(accountId) || 0) + pairedCount);
|
|
371
|
+
}
|
|
372
|
+
} catch {}
|
|
373
|
+
|
|
374
|
+
for (const accountId of counts.keys()) {
|
|
375
|
+
const accountConfig =
|
|
376
|
+
accountId === "default" &&
|
|
377
|
+
!(config.accounts && typeof config.accounts === "object")
|
|
378
|
+
? config
|
|
379
|
+
: config.accounts?.[accountId] || {};
|
|
380
|
+
const inlineAllowFrom = accountConfig?.allowFrom;
|
|
381
|
+
if (!Array.isArray(inlineAllowFrom)) continue;
|
|
382
|
+
counts.set(
|
|
383
|
+
accountId,
|
|
384
|
+
Number(counts.get(accountId) || 0) + inlineAllowFrom.length,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return counts;
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const listConfiguredChannelAccounts = ({ fsImpl, OPENCLAW_DIR, cfg }) => {
|
|
392
|
+
const bindings = Array.isArray(cfg?.bindings) ? cfg.bindings : [];
|
|
393
|
+
const boundAccountMap = new Map();
|
|
394
|
+
for (const binding of bindings) {
|
|
395
|
+
const match = binding?.match || {};
|
|
396
|
+
const hasScopedFields =
|
|
397
|
+
!!match.peer ||
|
|
398
|
+
!!match.parentPeer ||
|
|
399
|
+
!!String(match.guildId || "").trim() ||
|
|
400
|
+
!!String(match.teamId || "").trim() ||
|
|
401
|
+
(Array.isArray(match.roles) && match.roles.length > 0);
|
|
402
|
+
if (hasScopedFields) continue;
|
|
403
|
+
const channel = String(match.channel || "").trim();
|
|
404
|
+
if (!channel) continue;
|
|
405
|
+
const accountId = String(match.accountId || "").trim() || "default";
|
|
406
|
+
const agentId = String(binding?.agentId || "").trim();
|
|
407
|
+
if (!agentId) continue;
|
|
408
|
+
const key = `${channel}:${accountId}`;
|
|
409
|
+
if (!boundAccountMap.has(key)) {
|
|
410
|
+
boundAccountMap.set(key, agentId);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const channels =
|
|
414
|
+
cfg?.channels && typeof cfg.channels === "object" ? cfg.channels : {};
|
|
415
|
+
return Object.entries(channels)
|
|
416
|
+
.map(([channelId, channelConfig]) => {
|
|
417
|
+
const config =
|
|
418
|
+
channelConfig && typeof channelConfig === "object" ? channelConfig : {};
|
|
419
|
+
const accountsConfig =
|
|
420
|
+
config.accounts && typeof config.accounts === "object"
|
|
421
|
+
? config.accounts
|
|
422
|
+
: {};
|
|
423
|
+
const accountIds = Object.keys(accountsConfig)
|
|
424
|
+
.map((entry) => String(entry || "").trim())
|
|
425
|
+
.filter(Boolean);
|
|
426
|
+
const topLevelKeys = Object.keys(config).filter(
|
|
427
|
+
(entry) =>
|
|
428
|
+
entry !== "accounts" &&
|
|
429
|
+
entry !== "defaultAccount" &&
|
|
430
|
+
entry !== "enabled",
|
|
431
|
+
);
|
|
432
|
+
if (accountIds.length === 0 && topLevelKeys.length === 0) return null;
|
|
433
|
+
const normalizedAccountIds = accountIds.includes("default")
|
|
434
|
+
? accountIds
|
|
435
|
+
: topLevelKeys.length > 0
|
|
436
|
+
? ["default", ...accountIds]
|
|
437
|
+
: accountIds;
|
|
438
|
+
const pairedCounts = readPairedCountsByAccount({
|
|
439
|
+
fsImpl,
|
|
440
|
+
OPENCLAW_DIR,
|
|
441
|
+
channelId,
|
|
442
|
+
accountIds: normalizedAccountIds,
|
|
443
|
+
config,
|
|
444
|
+
});
|
|
445
|
+
return {
|
|
446
|
+
channel: String(channelId || "").trim(),
|
|
447
|
+
accounts: normalizedAccountIds
|
|
448
|
+
.map((accountId) => {
|
|
449
|
+
const accountConfig =
|
|
450
|
+
accountId === "default" && accountIds.length === 0
|
|
451
|
+
? config
|
|
452
|
+
: accountsConfig?.[accountId] || {};
|
|
453
|
+
return {
|
|
454
|
+
id: accountId,
|
|
455
|
+
name: String(accountConfig?.name || "").trim(),
|
|
456
|
+
envKey: deriveChannelEnvKey({ provider: channelId, accountId }),
|
|
457
|
+
boundAgentId:
|
|
458
|
+
boundAccountMap.get(
|
|
459
|
+
`${String(channelId || "").trim()}:${accountId}`,
|
|
460
|
+
) || "",
|
|
461
|
+
paired: Number(pairedCounts.get(accountId) || 0),
|
|
462
|
+
status:
|
|
463
|
+
Number(pairedCounts.get(accountId) || 0) > 0
|
|
464
|
+
? "paired"
|
|
465
|
+
: "configured",
|
|
466
|
+
};
|
|
467
|
+
}),
|
|
468
|
+
};
|
|
469
|
+
})
|
|
470
|
+
.filter(Boolean);
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const getSafeStat = ({ fsImpl, targetPath }) => {
|
|
474
|
+
try {
|
|
475
|
+
if (typeof fsImpl.lstatSync === "function") {
|
|
476
|
+
return fsImpl.lstatSync(targetPath);
|
|
477
|
+
}
|
|
478
|
+
if (typeof fsImpl.statSync === "function") {
|
|
479
|
+
return fsImpl.statSync(targetPath);
|
|
480
|
+
}
|
|
481
|
+
} catch {}
|
|
482
|
+
return null;
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const calculatePathSizeBytes = ({ fsImpl, targetPath }) => {
|
|
486
|
+
const stat = getSafeStat({ fsImpl, targetPath });
|
|
487
|
+
if (!stat) return 0;
|
|
488
|
+
if (typeof stat.isSymbolicLink === "function" && stat.isSymbolicLink())
|
|
489
|
+
return 0;
|
|
490
|
+
if (typeof stat.isFile === "function" && stat.isFile()) {
|
|
491
|
+
return Number(stat.size || 0);
|
|
492
|
+
}
|
|
493
|
+
if (!(typeof stat.isDirectory === "function" && stat.isDirectory())) {
|
|
494
|
+
return 0;
|
|
495
|
+
}
|
|
496
|
+
let entries = [];
|
|
497
|
+
try {
|
|
498
|
+
entries = fsImpl.readdirSync(targetPath) || [];
|
|
499
|
+
} catch {
|
|
500
|
+
return 0;
|
|
501
|
+
}
|
|
502
|
+
return entries.reduce(
|
|
503
|
+
(total, entry) =>
|
|
504
|
+
total +
|
|
505
|
+
calculatePathSizeBytes({
|
|
506
|
+
fsImpl,
|
|
507
|
+
targetPath: path.join(targetPath, String(entry || "")),
|
|
508
|
+
}),
|
|
509
|
+
0,
|
|
510
|
+
);
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
const getImplicitMainAgent = ({ OPENCLAW_DIR, cfg }) => {
|
|
514
|
+
const defaults = normalizeAgentDefaults({ cfg });
|
|
515
|
+
const defaultPrimaryModel = String(defaults?.model?.primary || "").trim();
|
|
516
|
+
return {
|
|
517
|
+
id: kDefaultAgentId,
|
|
518
|
+
default: true,
|
|
519
|
+
name: "Main Agent",
|
|
520
|
+
workspace: resolveAgentWorkspacePath({
|
|
521
|
+
OPENCLAW_DIR,
|
|
522
|
+
agentId: kDefaultAgentId,
|
|
523
|
+
}),
|
|
524
|
+
agentDir: resolveAgentDirPath({ OPENCLAW_DIR, agentId: kDefaultAgentId }),
|
|
525
|
+
...(defaultPrimaryModel ? { model: { primary: defaultPrimaryModel } } : {}),
|
|
526
|
+
};
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const withNormalizedAgentsConfig = ({ OPENCLAW_DIR, cfg }) => {
|
|
530
|
+
const nextCfg = cfg && typeof cfg === "object" ? { ...cfg } : {};
|
|
531
|
+
const existingAgents =
|
|
532
|
+
nextCfg.agents && typeof nextCfg.agents === "object" ? nextCfg.agents : {};
|
|
533
|
+
const existingList = normalizeAgentsList({ list: existingAgents.list });
|
|
534
|
+
const hasMain = existingList.some(
|
|
535
|
+
(entry) => String(entry.id || "").trim() === kDefaultAgentId,
|
|
536
|
+
);
|
|
537
|
+
const nextList = hasMain
|
|
538
|
+
? existingList
|
|
539
|
+
: [getImplicitMainAgent({ OPENCLAW_DIR, cfg: nextCfg }), ...existingList];
|
|
540
|
+
|
|
541
|
+
let hasDefault = false;
|
|
542
|
+
const listWithSingleDefault = nextList.map((entry) => {
|
|
543
|
+
if (!entry.default) return entry;
|
|
544
|
+
if (hasDefault) return { ...entry, default: false };
|
|
545
|
+
hasDefault = true;
|
|
546
|
+
return { ...entry, default: true };
|
|
547
|
+
});
|
|
548
|
+
if (!hasDefault && listWithSingleDefault.length > 0) {
|
|
549
|
+
listWithSingleDefault[0] = { ...listWithSingleDefault[0], default: true };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
nextCfg.agents = {
|
|
553
|
+
...existingAgents,
|
|
554
|
+
list: listWithSingleDefault,
|
|
555
|
+
};
|
|
556
|
+
return nextCfg;
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
const isValidAgentId = (value) =>
|
|
560
|
+
kAgentIdPattern.test(String(value || "").trim());
|
|
561
|
+
|
|
562
|
+
const isValidWorkspaceFolder = (value) =>
|
|
563
|
+
kWorkspaceFolderPattern.test(String(value || "").trim());
|
|
564
|
+
|
|
565
|
+
const resolveRequestedWorkspacePath = ({
|
|
566
|
+
OPENCLAW_DIR,
|
|
567
|
+
agentId,
|
|
568
|
+
workspaceFolder,
|
|
569
|
+
}) => {
|
|
570
|
+
const normalizedFolder = String(workspaceFolder || "").trim();
|
|
571
|
+
if (!normalizedFolder)
|
|
572
|
+
return resolveAgentWorkspacePath({ OPENCLAW_DIR, agentId });
|
|
573
|
+
if (!isValidWorkspaceFolder(normalizedFolder)) {
|
|
574
|
+
throw new Error(
|
|
575
|
+
"Workspace folder must be lowercase letters, numbers, and hyphens only",
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
return path.join(OPENCLAW_DIR, normalizedFolder);
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const ensureAgentScaffold = ({
|
|
582
|
+
fsImpl,
|
|
583
|
+
agentId,
|
|
584
|
+
workspacePath,
|
|
585
|
+
OPENCLAW_DIR,
|
|
586
|
+
}) => {
|
|
587
|
+
const agentDirPath = resolveAgentDirPath({ OPENCLAW_DIR, agentId });
|
|
588
|
+
fsImpl.mkdirSync(workspacePath, { recursive: true });
|
|
589
|
+
fsImpl.mkdirSync(agentDirPath, { recursive: true });
|
|
590
|
+
for (const fileName of kDefaultAgentFiles) {
|
|
591
|
+
const targetPath = path.join(workspacePath, fileName);
|
|
592
|
+
if (fsImpl.existsSync(targetPath)) continue;
|
|
593
|
+
fsImpl.writeFileSync(
|
|
594
|
+
targetPath,
|
|
595
|
+
`# ${fileName}\n\nCreated for agent "${agentId}".\n`,
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
workspacePath,
|
|
600
|
+
agentDirPath,
|
|
601
|
+
};
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
const createAgentsService = ({
|
|
605
|
+
fs: fsImpl = fs,
|
|
606
|
+
OPENCLAW_DIR,
|
|
607
|
+
readEnvFile = () => [],
|
|
608
|
+
writeEnvFile = () => {},
|
|
609
|
+
reloadEnv = () => false,
|
|
610
|
+
restartGateway = async () => {},
|
|
611
|
+
clawCmd = async () => ({
|
|
612
|
+
ok: false,
|
|
613
|
+
stdout: "",
|
|
614
|
+
stderr: "openclaw command unavailable",
|
|
615
|
+
}),
|
|
616
|
+
}) => {
|
|
617
|
+
const getChannelAccountToken = ({
|
|
618
|
+
provider: rawProvider,
|
|
619
|
+
accountId: rawAccountId,
|
|
620
|
+
} = {}) => {
|
|
621
|
+
const provider = normalizeChannelProvider(rawProvider);
|
|
622
|
+
const accountId = String(rawAccountId || "").trim() || "default";
|
|
623
|
+
const cfg = withNormalizedAgentsConfig({
|
|
624
|
+
OPENCLAW_DIR,
|
|
625
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
626
|
+
});
|
|
627
|
+
const providerConfig =
|
|
628
|
+
cfg.channels?.[provider] && typeof cfg.channels[provider] === "object"
|
|
629
|
+
? cfg.channels[provider]
|
|
630
|
+
: null;
|
|
631
|
+
if (!providerConfig) {
|
|
632
|
+
throw new Error(`Channel "${provider}" not found`);
|
|
633
|
+
}
|
|
634
|
+
const hasAccounts =
|
|
635
|
+
providerConfig.accounts && typeof providerConfig.accounts === "object";
|
|
636
|
+
const hasLegacyDefault =
|
|
637
|
+
accountId === "default" &&
|
|
638
|
+
!hasAccounts &&
|
|
639
|
+
hasLegacyDefaultChannelAccount({ config: providerConfig });
|
|
640
|
+
if (!hasLegacyDefault && !providerConfig.accounts?.[accountId]) {
|
|
641
|
+
throw new Error(`Channel account "${provider}/${accountId}" not found`);
|
|
642
|
+
}
|
|
643
|
+
const envKey = deriveChannelEnvKey({ provider, accountId });
|
|
644
|
+
const envVars = readEnvFile();
|
|
645
|
+
const envEntry = (Array.isArray(envVars) ? envVars : []).find(
|
|
646
|
+
(entry) => String(entry?.key || "").trim() === envKey,
|
|
647
|
+
);
|
|
648
|
+
return {
|
|
649
|
+
provider,
|
|
650
|
+
accountId,
|
|
651
|
+
envKey,
|
|
652
|
+
token: String(envEntry?.value || ""),
|
|
653
|
+
};
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
const listAgents = () => {
|
|
657
|
+
const cfg = withNormalizedAgentsConfig({
|
|
658
|
+
OPENCLAW_DIR,
|
|
659
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
660
|
+
});
|
|
661
|
+
return (cfg.agents?.list || []).map((entry) => ({
|
|
662
|
+
...entry,
|
|
663
|
+
id: String(entry.id || "").trim(),
|
|
664
|
+
name: String(entry.name || "").trim() || String(entry.id || "").trim(),
|
|
665
|
+
default: !!entry.default,
|
|
666
|
+
}));
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
const getAgent = (agentId) => {
|
|
670
|
+
const normalized = String(agentId || "").trim();
|
|
671
|
+
return listAgents().find((entry) => entry.id === normalized) || null;
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
const getAgentWorkspaceSize = (agentId) => {
|
|
675
|
+
const normalized = String(agentId || "").trim();
|
|
676
|
+
const agent = getAgent(normalized);
|
|
677
|
+
if (!agent) throw new Error(`Agent "${normalized}" not found`);
|
|
678
|
+
const workspacePath = String(
|
|
679
|
+
agent.workspace ||
|
|
680
|
+
resolveAgentWorkspacePath({ OPENCLAW_DIR, agentId: normalized }),
|
|
681
|
+
).trim();
|
|
682
|
+
if (!workspacePath) {
|
|
683
|
+
return { workspacePath: "", exists: false, sizeBytes: 0 };
|
|
684
|
+
}
|
|
685
|
+
const stat = getSafeStat({ fsImpl, targetPath: workspacePath });
|
|
686
|
+
if (!stat) {
|
|
687
|
+
return { workspacePath, exists: false, sizeBytes: 0 };
|
|
688
|
+
}
|
|
689
|
+
return {
|
|
690
|
+
workspacePath,
|
|
691
|
+
exists: true,
|
|
692
|
+
sizeBytes: calculatePathSizeBytes({ fsImpl, targetPath: workspacePath }),
|
|
693
|
+
};
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
const getBindingsForAgent = (agentId) => {
|
|
697
|
+
const normalized = String(agentId || "").trim();
|
|
698
|
+
const cfg = withNormalizedAgentsConfig({
|
|
699
|
+
OPENCLAW_DIR,
|
|
700
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
701
|
+
});
|
|
702
|
+
const bindings = Array.isArray(cfg.bindings) ? cfg.bindings : [];
|
|
703
|
+
return bindings
|
|
704
|
+
.filter((binding) => String(binding?.agentId || "").trim() === normalized)
|
|
705
|
+
.map((binding) => cloneJson(binding));
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
const createAgent = (input = {}) => {
|
|
709
|
+
const agentId = String(input.id || "").trim();
|
|
710
|
+
if (!isValidAgentId(agentId)) {
|
|
711
|
+
throw new Error(
|
|
712
|
+
"Agent id must be lowercase letters, numbers, and hyphens only",
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const cfg = withNormalizedAgentsConfig({
|
|
717
|
+
OPENCLAW_DIR,
|
|
718
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
719
|
+
});
|
|
720
|
+
const existing = cfg.agents.list.find((entry) => entry.id === agentId);
|
|
721
|
+
if (existing) {
|
|
722
|
+
throw new Error(`Agent "${agentId}" already exists`);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const workspacePath = resolveRequestedWorkspacePath({
|
|
726
|
+
OPENCLAW_DIR,
|
|
727
|
+
agentId,
|
|
728
|
+
workspaceFolder: input.workspaceFolder,
|
|
729
|
+
});
|
|
730
|
+
const { workspacePath: scaffoldWorkspacePath, agentDirPath } =
|
|
731
|
+
ensureAgentScaffold({
|
|
732
|
+
fsImpl,
|
|
733
|
+
workspacePath,
|
|
734
|
+
OPENCLAW_DIR,
|
|
735
|
+
agentId,
|
|
736
|
+
});
|
|
737
|
+
const nextAgent = {
|
|
738
|
+
id: agentId,
|
|
739
|
+
name: String(input.name || "").trim() || agentId,
|
|
740
|
+
default: false,
|
|
741
|
+
workspace: scaffoldWorkspacePath,
|
|
742
|
+
agentDir: agentDirPath,
|
|
743
|
+
...(input.model ? { model: input.model } : {}),
|
|
744
|
+
...(input.identity && typeof input.identity === "object"
|
|
745
|
+
? { identity: { ...input.identity } }
|
|
746
|
+
: {}),
|
|
747
|
+
};
|
|
748
|
+
cfg.agents.list = [...cfg.agents.list, nextAgent];
|
|
749
|
+
saveConfig({ fsImpl, OPENCLAW_DIR, config: cfg });
|
|
750
|
+
return nextAgent;
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
const updateAgent = (agentId, patch = {}) => {
|
|
754
|
+
const normalized = String(agentId || "").trim();
|
|
755
|
+
const cfg = withNormalizedAgentsConfig({
|
|
756
|
+
OPENCLAW_DIR,
|
|
757
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
758
|
+
});
|
|
759
|
+
const index = cfg.agents.list.findIndex((entry) => entry.id === normalized);
|
|
760
|
+
if (index < 0) throw new Error(`Agent "${normalized}" not found`);
|
|
761
|
+
const current = cfg.agents.list[index];
|
|
762
|
+
const next = {
|
|
763
|
+
...current,
|
|
764
|
+
...(patch.name !== undefined
|
|
765
|
+
? { name: String(patch.name || "").trim() }
|
|
766
|
+
: {}),
|
|
767
|
+
...(patch.identity !== undefined
|
|
768
|
+
? {
|
|
769
|
+
identity:
|
|
770
|
+
patch.identity && typeof patch.identity === "object"
|
|
771
|
+
? { ...patch.identity }
|
|
772
|
+
: {},
|
|
773
|
+
}
|
|
774
|
+
: {}),
|
|
775
|
+
};
|
|
776
|
+
if (patch.model !== undefined) {
|
|
777
|
+
if (patch.model === null) {
|
|
778
|
+
delete next.model;
|
|
779
|
+
} else {
|
|
780
|
+
next.model = patch.model;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
if (!String(next.name || "").trim()) next.name = normalized;
|
|
784
|
+
cfg.agents.list[index] = next;
|
|
785
|
+
saveConfig({ fsImpl, OPENCLAW_DIR, config: cfg });
|
|
786
|
+
return next;
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
const setDefaultAgent = (agentId) => {
|
|
790
|
+
const normalized = String(agentId || "").trim();
|
|
791
|
+
const cfg = withNormalizedAgentsConfig({
|
|
792
|
+
OPENCLAW_DIR,
|
|
793
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
794
|
+
});
|
|
795
|
+
const exists = cfg.agents.list.some((entry) => entry.id === normalized);
|
|
796
|
+
if (!exists) throw new Error(`Agent "${normalized}" not found`);
|
|
797
|
+
cfg.agents.list = cfg.agents.list.map((entry) => ({
|
|
798
|
+
...entry,
|
|
799
|
+
default: entry.id === normalized,
|
|
800
|
+
}));
|
|
801
|
+
saveConfig({ fsImpl, OPENCLAW_DIR, config: cfg });
|
|
802
|
+
return cfg.agents.list.find((entry) => entry.id === normalized) || null;
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
const addBinding = (agentId, input = {}) => {
|
|
806
|
+
const normalizedAgentId = String(agentId || "").trim();
|
|
807
|
+
const cfg = withNormalizedAgentsConfig({
|
|
808
|
+
OPENCLAW_DIR,
|
|
809
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
810
|
+
});
|
|
811
|
+
const agent = cfg.agents.list.find(
|
|
812
|
+
(entry) => entry.id === normalizedAgentId,
|
|
813
|
+
);
|
|
814
|
+
if (!agent) throw new Error(`Agent "${normalizedAgentId}" not found`);
|
|
815
|
+
const match = normalizeBindingMatch(input);
|
|
816
|
+
const nextBinding = appendBindingToConfig({
|
|
817
|
+
cfg,
|
|
818
|
+
agentId: normalizedAgentId,
|
|
819
|
+
match,
|
|
820
|
+
});
|
|
821
|
+
saveConfig({ fsImpl, OPENCLAW_DIR, config: cfg });
|
|
822
|
+
return nextBinding;
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
const removeBinding = (agentId, input = {}) => {
|
|
826
|
+
const normalizedAgentId = String(agentId || "").trim();
|
|
827
|
+
const cfg = withNormalizedAgentsConfig({
|
|
828
|
+
OPENCLAW_DIR,
|
|
829
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
830
|
+
});
|
|
831
|
+
const bindings = Array.isArray(cfg.bindings) ? cfg.bindings : [];
|
|
832
|
+
const nextMatch = normalizeBindingMatch(input);
|
|
833
|
+
const nextBindings = bindings.filter(
|
|
834
|
+
(binding) =>
|
|
835
|
+
!(
|
|
836
|
+
String(binding?.agentId || "").trim() === normalizedAgentId &&
|
|
837
|
+
matchesBinding(binding?.match || {}, nextMatch)
|
|
838
|
+
),
|
|
839
|
+
);
|
|
840
|
+
if (nextBindings.length === bindings.length) {
|
|
841
|
+
throw new Error("Binding not found");
|
|
842
|
+
}
|
|
843
|
+
cfg.bindings = nextBindings;
|
|
844
|
+
saveConfig({ fsImpl, OPENCLAW_DIR, config: cfg });
|
|
845
|
+
return { ok: true };
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
const createChannelAccount = async (
|
|
849
|
+
input = {},
|
|
850
|
+
{ onProgress = () => {} } = {},
|
|
851
|
+
) => {
|
|
852
|
+
const provider = normalizeChannelProvider(input.provider);
|
|
853
|
+
const name =
|
|
854
|
+
String(input.name || "").trim() || kChannelLabels[provider] || provider;
|
|
855
|
+
const token = String(input.token || "").trim();
|
|
856
|
+
if (!token) throw new Error("Channel token is required");
|
|
857
|
+
|
|
858
|
+
const cfg = withNormalizedAgentsConfig({
|
|
859
|
+
OPENCLAW_DIR,
|
|
860
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
const agentId = String(input.agentId || "").trim();
|
|
864
|
+
const agent = cfg.agents.list.find((entry) => entry.id === agentId);
|
|
865
|
+
if (!agent) throw new Error(`Agent "${agentId}" not found`);
|
|
866
|
+
|
|
867
|
+
const existingChannelConfig =
|
|
868
|
+
cfg.channels?.[provider] && typeof cfg.channels[provider] === "object"
|
|
869
|
+
? cfg.channels[provider]
|
|
870
|
+
: {};
|
|
871
|
+
const normalizedChannelConfig = normalizeChannelConfig({
|
|
872
|
+
provider,
|
|
873
|
+
channelConfig: existingChannelConfig,
|
|
874
|
+
});
|
|
875
|
+
const existingAccounts =
|
|
876
|
+
normalizedChannelConfig.accounts &&
|
|
877
|
+
typeof normalizedChannelConfig.accounts === "object"
|
|
878
|
+
? normalizedChannelConfig.accounts
|
|
879
|
+
: {};
|
|
880
|
+
const requestedAccountId = String(input.accountId || "").trim();
|
|
881
|
+
const accountId =
|
|
882
|
+
requestedAccountId ||
|
|
883
|
+
(Object.keys(existingAccounts).length > 0 ? "" : "default");
|
|
884
|
+
if (!accountId) {
|
|
885
|
+
throw new Error("Channel account id is required");
|
|
886
|
+
}
|
|
887
|
+
if (!isValidChannelAccountId(accountId)) {
|
|
888
|
+
throw new Error(
|
|
889
|
+
"Channel account id must be lowercase letters, numbers, and hyphens only",
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
if (existingAccounts[accountId]) {
|
|
893
|
+
throw new Error(
|
|
894
|
+
`Channel account "${provider}/${accountId}" already exists`,
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
if (provider === "discord" && Object.keys(existingAccounts).length > 0) {
|
|
898
|
+
throw new Error("Discord supports a single channel account");
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const envKey = deriveChannelEnvKey({ provider, accountId });
|
|
902
|
+
const tokenField = kChannelTokenFields[provider];
|
|
903
|
+
const currentEnvVars = readEnvFile();
|
|
904
|
+
const previousEnvVars = Array.isArray(currentEnvVars) ? currentEnvVars : [];
|
|
905
|
+
const duplicateEnvEntry = previousEnvVars.find((entry) => {
|
|
906
|
+
const existingKey = String(entry?.key || "").trim();
|
|
907
|
+
const existingValue = String(entry?.value || "").trim();
|
|
908
|
+
if (!existingKey || !existingValue) return false;
|
|
909
|
+
if (existingKey === envKey) return false;
|
|
910
|
+
return existingValue === token;
|
|
911
|
+
});
|
|
912
|
+
let orphanedEnvKey = null;
|
|
913
|
+
if (duplicateEnvEntry) {
|
|
914
|
+
const dupKey = String(duplicateEnvEntry.key || "").trim();
|
|
915
|
+
const configuredKeys = getConfiguredChannelEnvKeys(cfg);
|
|
916
|
+
if (configuredKeys.has(dupKey)) {
|
|
917
|
+
throw new Error(`Channel token already exists in ${dupKey}`);
|
|
918
|
+
}
|
|
919
|
+
orphanedEnvKey = dupKey;
|
|
920
|
+
console.log(
|
|
921
|
+
`[alphaclaw] Overwriting orphaned channel env var ${dupKey} (no matching config entry)`,
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
const nextEnvVars = previousEnvVars.filter((entry) => {
|
|
925
|
+
const key = String(entry?.key || "").trim();
|
|
926
|
+
return key !== envKey && key !== orphanedEnvKey;
|
|
927
|
+
});
|
|
928
|
+
nextEnvVars.push({ key: envKey, value: token });
|
|
929
|
+
|
|
930
|
+
const previousConfig = cloneJson(cfg);
|
|
931
|
+
try {
|
|
932
|
+
onProgress({ phase: "restarting", label: "Rebooting..." });
|
|
933
|
+
writeEnvFile(nextEnvVars);
|
|
934
|
+
reloadEnv();
|
|
935
|
+
assertActiveChannelTokenEnvVars({
|
|
936
|
+
cfg: withNormalizedAgentsConfig({
|
|
937
|
+
OPENCLAW_DIR,
|
|
938
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
939
|
+
}),
|
|
940
|
+
envVars: nextEnvVars,
|
|
941
|
+
});
|
|
942
|
+
await restartGateway();
|
|
943
|
+
const pluginEnabledCfg = withNormalizedAgentsConfig({
|
|
944
|
+
OPENCLAW_DIR,
|
|
945
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
946
|
+
});
|
|
947
|
+
ensurePluginAllowed({ cfg: pluginEnabledCfg, pluginKey: provider });
|
|
948
|
+
saveConfig({ fsImpl, OPENCLAW_DIR, config: pluginEnabledCfg });
|
|
949
|
+
const addArgs = [
|
|
950
|
+
"channels add",
|
|
951
|
+
`--channel ${shellEscapeArg(provider)}`,
|
|
952
|
+
accountId !== "default" ? `--account ${shellEscapeArg(accountId)}` : "",
|
|
953
|
+
name ? `--name ${shellEscapeArg(name)}` : "",
|
|
954
|
+
`--token ${shellEscapeArg(token)}`,
|
|
955
|
+
].filter(Boolean);
|
|
956
|
+
const addResult = await clawCmd(addArgs.join(" "), {
|
|
957
|
+
quiet: true,
|
|
958
|
+
timeoutMs: 30000,
|
|
959
|
+
});
|
|
960
|
+
if (!addResult?.ok) {
|
|
961
|
+
throw new Error(
|
|
962
|
+
addResult?.stderr ||
|
|
963
|
+
addResult?.stdout ||
|
|
964
|
+
"Could not add channel account",
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
const nextCfg = withNormalizedAgentsConfig({
|
|
968
|
+
OPENCLAW_DIR,
|
|
969
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
970
|
+
});
|
|
971
|
+
const nextProviderConfig = normalizeChannelConfig({
|
|
972
|
+
provider,
|
|
973
|
+
channelConfig:
|
|
974
|
+
nextCfg.channels?.[provider] &&
|
|
975
|
+
typeof nextCfg.channels[provider] === "object"
|
|
976
|
+
? nextCfg.channels[provider]
|
|
977
|
+
: {},
|
|
978
|
+
});
|
|
979
|
+
const nextAccounts =
|
|
980
|
+
nextProviderConfig.accounts &&
|
|
981
|
+
typeof nextProviderConfig.accounts === "object"
|
|
982
|
+
? { ...nextProviderConfig.accounts }
|
|
983
|
+
: {};
|
|
984
|
+
nextAccounts[accountId] = {
|
|
985
|
+
...(nextAccounts[accountId] &&
|
|
986
|
+
typeof nextAccounts[accountId] === "object"
|
|
987
|
+
? nextAccounts[accountId]
|
|
988
|
+
: {}),
|
|
989
|
+
...(name ? { name } : {}),
|
|
990
|
+
[tokenField]: `\${${envKey}}`,
|
|
991
|
+
dmPolicy: "pairing",
|
|
992
|
+
};
|
|
993
|
+
nextProviderConfig.accounts = nextAccounts;
|
|
994
|
+
nextProviderConfig.enabled = true;
|
|
995
|
+
if (
|
|
996
|
+
nextProviderConfig.accounts &&
|
|
997
|
+
typeof nextProviderConfig.accounts === "object" &&
|
|
998
|
+
!String(nextProviderConfig.defaultAccount || "").trim()
|
|
999
|
+
) {
|
|
1000
|
+
nextProviderConfig.defaultAccount = "default";
|
|
1001
|
+
}
|
|
1002
|
+
nextCfg.channels =
|
|
1003
|
+
nextCfg.channels && typeof nextCfg.channels === "object"
|
|
1004
|
+
? { ...nextCfg.channels }
|
|
1005
|
+
: {};
|
|
1006
|
+
nextCfg.channels[provider] = nextProviderConfig;
|
|
1007
|
+
saveConfig({ fsImpl, OPENCLAW_DIR, config: nextCfg });
|
|
1008
|
+
onProgress({ phase: "binding", label: "Binding agent..." });
|
|
1009
|
+
const bindSpec = buildBindingSpec({ provider, accountId });
|
|
1010
|
+
const bindResult = await clawCmd(
|
|
1011
|
+
`agents bind --agent ${shellEscapeArg(agentId)} --bind ${shellEscapeArg(bindSpec)}`,
|
|
1012
|
+
{ quiet: true, timeoutMs: 30000 },
|
|
1013
|
+
);
|
|
1014
|
+
if (!bindResult?.ok) {
|
|
1015
|
+
throw new Error(
|
|
1016
|
+
bindResult?.stderr ||
|
|
1017
|
+
bindResult?.stdout ||
|
|
1018
|
+
"Could not bind channel account",
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
try {
|
|
1023
|
+
await clawCmd(
|
|
1024
|
+
[
|
|
1025
|
+
"channels remove",
|
|
1026
|
+
`--channel ${shellEscapeArg(provider)}`,
|
|
1027
|
+
accountId !== "default"
|
|
1028
|
+
? `--account ${shellEscapeArg(accountId)}`
|
|
1029
|
+
: "",
|
|
1030
|
+
"--delete",
|
|
1031
|
+
]
|
|
1032
|
+
.filter(Boolean)
|
|
1033
|
+
.join(" "),
|
|
1034
|
+
{ quiet: true, timeoutMs: 30000 },
|
|
1035
|
+
);
|
|
1036
|
+
} catch {}
|
|
1037
|
+
try {
|
|
1038
|
+
writeEnvFile(previousEnvVars);
|
|
1039
|
+
reloadEnv();
|
|
1040
|
+
} catch {}
|
|
1041
|
+
try {
|
|
1042
|
+
saveConfig({ fsImpl, OPENCLAW_DIR, config: previousConfig });
|
|
1043
|
+
} catch {}
|
|
1044
|
+
throw error;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
const binding = {
|
|
1048
|
+
agentId,
|
|
1049
|
+
match: normalizeBindingMatch({
|
|
1050
|
+
channel: provider,
|
|
1051
|
+
accountId,
|
|
1052
|
+
}),
|
|
1053
|
+
};
|
|
1054
|
+
return {
|
|
1055
|
+
channel: provider,
|
|
1056
|
+
account: {
|
|
1057
|
+
id: accountId,
|
|
1058
|
+
name,
|
|
1059
|
+
envKey,
|
|
1060
|
+
},
|
|
1061
|
+
binding,
|
|
1062
|
+
};
|
|
1063
|
+
};
|
|
1064
|
+
|
|
1065
|
+
const updateChannelAccount = (input = {}) => {
|
|
1066
|
+
const provider = normalizeChannelProvider(input.provider);
|
|
1067
|
+
const accountId = String(input.accountId || "").trim() || "default";
|
|
1068
|
+
const nextName = String(input.name || "").trim();
|
|
1069
|
+
const nextAgentId = String(input.agentId || "").trim();
|
|
1070
|
+
const nextToken = String(input.token || "").trim();
|
|
1071
|
+
if (!nextName) throw new Error("Channel name is required");
|
|
1072
|
+
if (!nextAgentId) throw new Error("Agent is required");
|
|
1073
|
+
|
|
1074
|
+
const cfg = withNormalizedAgentsConfig({
|
|
1075
|
+
OPENCLAW_DIR,
|
|
1076
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
1077
|
+
});
|
|
1078
|
+
const agent = cfg.agents.list.find((entry) => entry.id === nextAgentId);
|
|
1079
|
+
if (!agent) throw new Error(`Agent "${nextAgentId}" not found`);
|
|
1080
|
+
|
|
1081
|
+
const providerConfig =
|
|
1082
|
+
cfg.channels?.[provider] && typeof cfg.channels[provider] === "object"
|
|
1083
|
+
? { ...cfg.channels[provider] }
|
|
1084
|
+
: null;
|
|
1085
|
+
if (!providerConfig) {
|
|
1086
|
+
throw new Error(`Channel "${provider}" not found`);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
const hasAccounts =
|
|
1090
|
+
providerConfig.accounts && typeof providerConfig.accounts === "object";
|
|
1091
|
+
const hasLegacyDefault =
|
|
1092
|
+
accountId === "default" &&
|
|
1093
|
+
!hasAccounts &&
|
|
1094
|
+
hasLegacyDefaultChannelAccount({ config: providerConfig });
|
|
1095
|
+
if (!hasLegacyDefault && !providerConfig.accounts?.[accountId]) {
|
|
1096
|
+
throw new Error(`Channel account "${provider}/${accountId}" not found`);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
let tokenUpdated = false;
|
|
1100
|
+
if (nextToken) {
|
|
1101
|
+
const envKey = deriveChannelEnvKey({ provider, accountId });
|
|
1102
|
+
const currentEnvVars = readEnvFile();
|
|
1103
|
+
const previousEnvVars = Array.isArray(currentEnvVars)
|
|
1104
|
+
? currentEnvVars
|
|
1105
|
+
: [];
|
|
1106
|
+
const existingToken = String(
|
|
1107
|
+
previousEnvVars.find(
|
|
1108
|
+
(entry) => String(entry?.key || "").trim() === envKey,
|
|
1109
|
+
)?.value || "",
|
|
1110
|
+
);
|
|
1111
|
+
const duplicateEnvEntry = previousEnvVars.find((entry) => {
|
|
1112
|
+
const existingKey = String(entry?.key || "").trim();
|
|
1113
|
+
const existingValue = String(entry?.value || "").trim();
|
|
1114
|
+
if (!existingKey || !existingValue) return false;
|
|
1115
|
+
if (existingKey === envKey) return false;
|
|
1116
|
+
return existingValue === nextToken;
|
|
1117
|
+
});
|
|
1118
|
+
if (duplicateEnvEntry) {
|
|
1119
|
+
const dupKey = String(duplicateEnvEntry.key || "").trim();
|
|
1120
|
+
const configuredKeys = getConfiguredChannelEnvKeys(cfg);
|
|
1121
|
+
if (configuredKeys.has(dupKey)) {
|
|
1122
|
+
throw new Error(`Channel token already exists in ${dupKey}`);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
if (existingToken !== nextToken) {
|
|
1126
|
+
const nextEnvVars = previousEnvVars.filter(
|
|
1127
|
+
(entry) => String(entry?.key || "").trim() !== envKey,
|
|
1128
|
+
);
|
|
1129
|
+
nextEnvVars.push({ key: envKey, value: nextToken });
|
|
1130
|
+
writeEnvFile(nextEnvVars);
|
|
1131
|
+
reloadEnv();
|
|
1132
|
+
tokenUpdated = true;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
if (hasLegacyDefault) {
|
|
1137
|
+
providerConfig.name = nextName;
|
|
1138
|
+
} else {
|
|
1139
|
+
providerConfig.accounts = { ...providerConfig.accounts };
|
|
1140
|
+
providerConfig.accounts[accountId] = {
|
|
1141
|
+
...(providerConfig.accounts[accountId] || {}),
|
|
1142
|
+
name: nextName,
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
cfg.channels =
|
|
1146
|
+
cfg.channels && typeof cfg.channels === "object"
|
|
1147
|
+
? { ...cfg.channels }
|
|
1148
|
+
: {};
|
|
1149
|
+
cfg.channels[provider] = providerConfig;
|
|
1150
|
+
|
|
1151
|
+
const bindings = Array.isArray(cfg.bindings) ? cfg.bindings : [];
|
|
1152
|
+
const targetMatch = normalizeBindingMatch({ channel: provider, accountId });
|
|
1153
|
+
const nextBindings = bindings.filter((binding) => {
|
|
1154
|
+
const match = binding?.match || {};
|
|
1155
|
+
const hasScopedFields =
|
|
1156
|
+
!!match.peer ||
|
|
1157
|
+
!!match.parentPeer ||
|
|
1158
|
+
!!String(match.guildId || "").trim() ||
|
|
1159
|
+
!!String(match.teamId || "").trim() ||
|
|
1160
|
+
(Array.isArray(match.roles) && match.roles.length > 0);
|
|
1161
|
+
if (hasScopedFields) return true;
|
|
1162
|
+
return !matchesBinding(match, targetMatch);
|
|
1163
|
+
});
|
|
1164
|
+
cfg.bindings = nextBindings;
|
|
1165
|
+
appendBindingToConfig({
|
|
1166
|
+
cfg,
|
|
1167
|
+
agentId: nextAgentId,
|
|
1168
|
+
match: targetMatch,
|
|
1169
|
+
});
|
|
1170
|
+
saveConfig({ fsImpl, OPENCLAW_DIR, config: cfg });
|
|
1171
|
+
|
|
1172
|
+
return {
|
|
1173
|
+
channel: provider,
|
|
1174
|
+
account: {
|
|
1175
|
+
id: accountId,
|
|
1176
|
+
name: nextName,
|
|
1177
|
+
boundAgentId: nextAgentId,
|
|
1178
|
+
},
|
|
1179
|
+
tokenUpdated,
|
|
1180
|
+
};
|
|
1181
|
+
};
|
|
1182
|
+
|
|
1183
|
+
const cleanupChannelAccountPairingFiles = ({ provider, accountId }) => {
|
|
1184
|
+
const credDir = resolveCredentialsDirPath({ OPENCLAW_DIR });
|
|
1185
|
+
const normalizedAccountId =
|
|
1186
|
+
String(accountId || "")
|
|
1187
|
+
.trim()
|
|
1188
|
+
.toLowerCase() || "default";
|
|
1189
|
+
|
|
1190
|
+
const pairingFilePath = path.join(credDir, `${provider}-pairing.json`);
|
|
1191
|
+
try {
|
|
1192
|
+
const raw = fsImpl.readFileSync(pairingFilePath, "utf8");
|
|
1193
|
+
const parsed = JSON.parse(raw);
|
|
1194
|
+
const requests = Array.isArray(parsed?.requests) ? parsed.requests : [];
|
|
1195
|
+
const nextRequests = requests.filter((entry) => {
|
|
1196
|
+
const entryAccountId =
|
|
1197
|
+
String(entry?.meta?.accountId || "")
|
|
1198
|
+
.trim()
|
|
1199
|
+
.toLowerCase() || "default";
|
|
1200
|
+
return entryAccountId !== normalizedAccountId;
|
|
1201
|
+
});
|
|
1202
|
+
if (nextRequests.length !== requests.length) {
|
|
1203
|
+
fsImpl.writeFileSync(
|
|
1204
|
+
pairingFilePath,
|
|
1205
|
+
JSON.stringify({ version: 1, requests: nextRequests }, null, 2),
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
} catch {}
|
|
1209
|
+
|
|
1210
|
+
const allowFromPatterns = [
|
|
1211
|
+
`${provider}-${normalizedAccountId}-allowFrom.json`,
|
|
1212
|
+
...(normalizedAccountId === "default"
|
|
1213
|
+
? [`${provider}-allowFrom.json`]
|
|
1214
|
+
: []),
|
|
1215
|
+
];
|
|
1216
|
+
for (const fileName of allowFromPatterns) {
|
|
1217
|
+
try {
|
|
1218
|
+
fsImpl.rmSync(path.join(credDir, fileName), { force: true });
|
|
1219
|
+
} catch {}
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
|
|
1223
|
+
const deleteChannelAccount = async (input = {}) => {
|
|
1224
|
+
const provider = normalizeChannelProvider(input.provider);
|
|
1225
|
+
const accountId = String(input.accountId || "").trim() || "default";
|
|
1226
|
+
|
|
1227
|
+
const cfg = withNormalizedAgentsConfig({
|
|
1228
|
+
OPENCLAW_DIR,
|
|
1229
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
1230
|
+
});
|
|
1231
|
+
const providerConfig =
|
|
1232
|
+
cfg.channels?.[provider] && typeof cfg.channels[provider] === "object"
|
|
1233
|
+
? cfg.channels[provider]
|
|
1234
|
+
: null;
|
|
1235
|
+
if (!providerConfig) {
|
|
1236
|
+
throw new Error(`Channel "${provider}" not found`);
|
|
1237
|
+
}
|
|
1238
|
+
const hasAccounts =
|
|
1239
|
+
providerConfig.accounts && typeof providerConfig.accounts === "object";
|
|
1240
|
+
const hasLegacyDefault =
|
|
1241
|
+
accountId === "default" &&
|
|
1242
|
+
!hasAccounts &&
|
|
1243
|
+
hasLegacyDefaultChannelAccount({ config: providerConfig });
|
|
1244
|
+
if (!hasLegacyDefault && !providerConfig.accounts?.[accountId]) {
|
|
1245
|
+
throw new Error(`Channel account "${provider}/${accountId}" not found`);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
if (provider === "discord") {
|
|
1249
|
+
const nextCfg = withNormalizedAgentsConfig({
|
|
1250
|
+
OPENCLAW_DIR,
|
|
1251
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
1252
|
+
});
|
|
1253
|
+
const nextChannels =
|
|
1254
|
+
nextCfg.channels && typeof nextCfg.channels === "object"
|
|
1255
|
+
? { ...nextCfg.channels }
|
|
1256
|
+
: {};
|
|
1257
|
+
const nextProviderConfig = normalizeChannelConfig({
|
|
1258
|
+
provider,
|
|
1259
|
+
channelConfig:
|
|
1260
|
+
nextChannels[provider] && typeof nextChannels[provider] === "object"
|
|
1261
|
+
? nextChannels[provider]
|
|
1262
|
+
: {},
|
|
1263
|
+
});
|
|
1264
|
+
const nextAccounts =
|
|
1265
|
+
nextProviderConfig.accounts &&
|
|
1266
|
+
typeof nextProviderConfig.accounts === "object"
|
|
1267
|
+
? { ...nextProviderConfig.accounts }
|
|
1268
|
+
: {};
|
|
1269
|
+
delete nextAccounts[accountId];
|
|
1270
|
+
if (Object.keys(nextAccounts).length > 0) {
|
|
1271
|
+
nextProviderConfig.accounts = nextAccounts;
|
|
1272
|
+
nextChannels[provider] = nextProviderConfig;
|
|
1273
|
+
} else {
|
|
1274
|
+
delete nextChannels[provider];
|
|
1275
|
+
}
|
|
1276
|
+
nextCfg.channels = nextChannels;
|
|
1277
|
+
|
|
1278
|
+
const targetMatch = normalizeBindingMatch({
|
|
1279
|
+
channel: provider,
|
|
1280
|
+
accountId,
|
|
1281
|
+
});
|
|
1282
|
+
const existingBindings = Array.isArray(nextCfg.bindings)
|
|
1283
|
+
? nextCfg.bindings
|
|
1284
|
+
: [];
|
|
1285
|
+
nextCfg.bindings = existingBindings.filter((binding) => {
|
|
1286
|
+
const match = binding?.match || {};
|
|
1287
|
+
const hasScopedFields =
|
|
1288
|
+
!!match.peer ||
|
|
1289
|
+
!!match.parentPeer ||
|
|
1290
|
+
!!String(match.guildId || "").trim() ||
|
|
1291
|
+
!!String(match.teamId || "").trim() ||
|
|
1292
|
+
(Array.isArray(match.roles) && match.roles.length > 0);
|
|
1293
|
+
if (hasScopedFields) return true;
|
|
1294
|
+
return !matchesBinding(match, targetMatch);
|
|
1295
|
+
});
|
|
1296
|
+
if (!nextChannels[provider] && nextCfg.plugins?.entries?.[provider]) {
|
|
1297
|
+
nextCfg.plugins.entries[provider] = {
|
|
1298
|
+
...(nextCfg.plugins.entries[provider] || {}),
|
|
1299
|
+
enabled: false,
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
saveConfig({ fsImpl, OPENCLAW_DIR, config: nextCfg });
|
|
1303
|
+
|
|
1304
|
+
const envKey = deriveChannelEnvKey({ provider, accountId });
|
|
1305
|
+
const currentEnvVars = readEnvFile();
|
|
1306
|
+
const previousEnvVars = Array.isArray(currentEnvVars)
|
|
1307
|
+
? currentEnvVars
|
|
1308
|
+
: [];
|
|
1309
|
+
const nextEnvVars = previousEnvVars.filter(
|
|
1310
|
+
(entry) => String(entry?.key || "").trim() !== envKey,
|
|
1311
|
+
);
|
|
1312
|
+
if (nextEnvVars.length !== previousEnvVars.length) {
|
|
1313
|
+
writeEnvFile(nextEnvVars);
|
|
1314
|
+
reloadEnv();
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
cleanupChannelAccountPairingFiles({ provider, accountId });
|
|
1318
|
+
return { ok: true };
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
const removeArgs = [
|
|
1322
|
+
"channels remove",
|
|
1323
|
+
`--channel ${shellEscapeArg(provider)}`,
|
|
1324
|
+
`--account ${shellEscapeArg(accountId)}`,
|
|
1325
|
+
"--delete",
|
|
1326
|
+
].filter(Boolean);
|
|
1327
|
+
const removeResult = await clawCmd(removeArgs.join(" "), {
|
|
1328
|
+
quiet: true,
|
|
1329
|
+
timeoutMs: 30000,
|
|
1330
|
+
});
|
|
1331
|
+
if (!removeResult?.ok) {
|
|
1332
|
+
throw new Error(
|
|
1333
|
+
removeResult?.stderr ||
|
|
1334
|
+
removeResult?.stdout ||
|
|
1335
|
+
"Could not delete channel account",
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
const envKey = deriveChannelEnvKey({ provider, accountId });
|
|
1340
|
+
const currentEnvVars = readEnvFile();
|
|
1341
|
+
const previousEnvVars = Array.isArray(currentEnvVars) ? currentEnvVars : [];
|
|
1342
|
+
const nextEnvVars = previousEnvVars.filter(
|
|
1343
|
+
(entry) => String(entry?.key || "").trim() !== envKey,
|
|
1344
|
+
);
|
|
1345
|
+
if (nextEnvVars.length !== previousEnvVars.length) {
|
|
1346
|
+
writeEnvFile(nextEnvVars);
|
|
1347
|
+
reloadEnv();
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
const nextCfg = withNormalizedAgentsConfig({
|
|
1351
|
+
OPENCLAW_DIR,
|
|
1352
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
1353
|
+
});
|
|
1354
|
+
const nextChannels =
|
|
1355
|
+
nextCfg.channels && typeof nextCfg.channels === "object"
|
|
1356
|
+
? { ...nextCfg.channels }
|
|
1357
|
+
: {};
|
|
1358
|
+
const nextProviderConfig = normalizeChannelConfig({
|
|
1359
|
+
provider,
|
|
1360
|
+
channelConfig:
|
|
1361
|
+
nextChannels[provider] && typeof nextChannels[provider] === "object"
|
|
1362
|
+
? nextChannels[provider]
|
|
1363
|
+
: {},
|
|
1364
|
+
});
|
|
1365
|
+
const nextAccounts =
|
|
1366
|
+
nextProviderConfig.accounts &&
|
|
1367
|
+
typeof nextProviderConfig.accounts === "object"
|
|
1368
|
+
? { ...nextProviderConfig.accounts }
|
|
1369
|
+
: {};
|
|
1370
|
+
delete nextAccounts[accountId];
|
|
1371
|
+
if (Object.keys(nextAccounts).length > 0) {
|
|
1372
|
+
nextProviderConfig.accounts = nextAccounts;
|
|
1373
|
+
nextChannels[provider] = nextProviderConfig;
|
|
1374
|
+
} else {
|
|
1375
|
+
delete nextChannels[provider];
|
|
1376
|
+
}
|
|
1377
|
+
nextCfg.channels = nextChannels;
|
|
1378
|
+
const targetMatch = normalizeBindingMatch({ channel: provider, accountId });
|
|
1379
|
+
const existingBindings = Array.isArray(nextCfg.bindings)
|
|
1380
|
+
? nextCfg.bindings
|
|
1381
|
+
: [];
|
|
1382
|
+
nextCfg.bindings = existingBindings.filter((binding) => {
|
|
1383
|
+
const match = binding?.match || {};
|
|
1384
|
+
const hasScopedFields =
|
|
1385
|
+
!!match.peer ||
|
|
1386
|
+
!!match.parentPeer ||
|
|
1387
|
+
!!String(match.guildId || "").trim() ||
|
|
1388
|
+
!!String(match.teamId || "").trim() ||
|
|
1389
|
+
(Array.isArray(match.roles) && match.roles.length > 0);
|
|
1390
|
+
if (hasScopedFields) return true;
|
|
1391
|
+
return !matchesBinding(match, targetMatch);
|
|
1392
|
+
});
|
|
1393
|
+
saveConfig({ fsImpl, OPENCLAW_DIR, config: nextCfg });
|
|
1394
|
+
|
|
1395
|
+
cleanupChannelAccountPairingFiles({ provider, accountId });
|
|
1396
|
+
return { ok: true };
|
|
1397
|
+
};
|
|
1398
|
+
|
|
1399
|
+
const deleteAgent = (agentId, { keepWorkspace = true } = {}) => {
|
|
1400
|
+
const normalized = String(agentId || "").trim();
|
|
1401
|
+
if (!normalized || normalized === kDefaultAgentId) {
|
|
1402
|
+
throw new Error("The default main agent cannot be deleted");
|
|
1403
|
+
}
|
|
1404
|
+
const cfg = withNormalizedAgentsConfig({
|
|
1405
|
+
OPENCLAW_DIR,
|
|
1406
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
1407
|
+
});
|
|
1408
|
+
const target = cfg.agents.list.find((entry) => entry.id === normalized);
|
|
1409
|
+
if (!target) throw new Error(`Agent "${normalized}" not found`);
|
|
1410
|
+
if (target.default) {
|
|
1411
|
+
throw new Error("Default agent cannot be deleted");
|
|
1412
|
+
}
|
|
1413
|
+
cfg.agents.list = cfg.agents.list.filter(
|
|
1414
|
+
(entry) => entry.id !== normalized,
|
|
1415
|
+
);
|
|
1416
|
+
if (Array.isArray(cfg.bindings)) {
|
|
1417
|
+
cfg.bindings = cfg.bindings.filter(
|
|
1418
|
+
(binding) => String(binding?.agentId || "") !== normalized,
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
saveConfig({ fsImpl, OPENCLAW_DIR, config: cfg });
|
|
1422
|
+
|
|
1423
|
+
if (!keepWorkspace) {
|
|
1424
|
+
const workspacePath = resolveAgentWorkspacePath({
|
|
1425
|
+
OPENCLAW_DIR,
|
|
1426
|
+
agentId: normalized,
|
|
1427
|
+
});
|
|
1428
|
+
const agentDirPath = path.join(OPENCLAW_DIR, "agents", normalized);
|
|
1429
|
+
fsImpl.rmSync(workspacePath, { recursive: true, force: true });
|
|
1430
|
+
fsImpl.rmSync(agentDirPath, { recursive: true, force: true });
|
|
1431
|
+
}
|
|
1432
|
+
return { ok: true };
|
|
1433
|
+
};
|
|
1434
|
+
|
|
1435
|
+
return {
|
|
1436
|
+
listAgents,
|
|
1437
|
+
getAgent,
|
|
1438
|
+
getAgentWorkspaceSize,
|
|
1439
|
+
getBindingsForAgent,
|
|
1440
|
+
getChannelAccountToken,
|
|
1441
|
+
createAgent,
|
|
1442
|
+
updateAgent,
|
|
1443
|
+
setDefaultAgent,
|
|
1444
|
+
addBinding,
|
|
1445
|
+
removeBinding,
|
|
1446
|
+
createChannelAccount,
|
|
1447
|
+
updateChannelAccount,
|
|
1448
|
+
deleteChannelAccount,
|
|
1449
|
+
listConfiguredChannelAccounts: () => {
|
|
1450
|
+
const channels = listConfiguredChannelAccounts({
|
|
1451
|
+
fsImpl,
|
|
1452
|
+
OPENCLAW_DIR,
|
|
1453
|
+
cfg: withNormalizedAgentsConfig({
|
|
1454
|
+
OPENCLAW_DIR,
|
|
1455
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
1456
|
+
}),
|
|
1457
|
+
});
|
|
1458
|
+
const envVars = readEnvFile();
|
|
1459
|
+
const envKeySet = new Set(
|
|
1460
|
+
(Array.isArray(envVars) ? envVars : [])
|
|
1461
|
+
.filter((v) => v?.key && String(v?.value || "").trim())
|
|
1462
|
+
.map((v) => String(v.key).trim()),
|
|
1463
|
+
);
|
|
1464
|
+
return channels.map((entry) => ({
|
|
1465
|
+
...entry,
|
|
1466
|
+
accounts: entry.accounts.map((account) => ({
|
|
1467
|
+
...account,
|
|
1468
|
+
token: envKeySet.has(String(account.envKey || "").trim())
|
|
1469
|
+
? kMaskedChannelToken
|
|
1470
|
+
: "",
|
|
1471
|
+
})),
|
|
1472
|
+
}));
|
|
1473
|
+
},
|
|
1474
|
+
deleteAgent,
|
|
1475
|
+
};
|
|
1476
|
+
};
|
|
1477
|
+
|
|
1478
|
+
module.exports = { createAgentsService };
|