@chrysb/alphaclaw 0.6.0-beta.1 → 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/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 -1458
- 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 +1 -1
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useEffect, useMemo, useState } from "https://esm.sh/preact/hooks";
|
|
3
|
+
import htm from "https://esm.sh/htm";
|
|
4
|
+
import {
|
|
5
|
+
canAgentBindAccount,
|
|
6
|
+
getChannelItemSortRank,
|
|
7
|
+
getResolvedAccountStatusInfo,
|
|
8
|
+
isImplicitDefaultAccount,
|
|
9
|
+
resolveChannelAccountLabel,
|
|
10
|
+
} from "./helpers.js";
|
|
11
|
+
|
|
12
|
+
const html = htm.bind(h);
|
|
13
|
+
|
|
14
|
+
export const useChannelItems = ({
|
|
15
|
+
agentId = "",
|
|
16
|
+
agentNameMap = new Map(),
|
|
17
|
+
channelStatus = {},
|
|
18
|
+
configuredChannelMap = new Map(),
|
|
19
|
+
configuredChannels = [],
|
|
20
|
+
defaultAgentId = "",
|
|
21
|
+
isDefaultAgent = false,
|
|
22
|
+
}) => {
|
|
23
|
+
const hasDiscordAccount = useMemo(() => {
|
|
24
|
+
const discordChannel = configuredChannelMap.get("discord");
|
|
25
|
+
return Array.isArray(discordChannel?.accounts) && discordChannel.accounts.length > 0;
|
|
26
|
+
}, [configuredChannelMap]);
|
|
27
|
+
|
|
28
|
+
const [showAssignedElsewhere, setShowAssignedElsewhere] = useState(false);
|
|
29
|
+
|
|
30
|
+
const channelItemData = useMemo(() => {
|
|
31
|
+
const channelOrderMap = new Map(
|
|
32
|
+
configuredChannels.map((entry, index) => [
|
|
33
|
+
String(entry?.channel || "").trim(),
|
|
34
|
+
index,
|
|
35
|
+
]),
|
|
36
|
+
);
|
|
37
|
+
const accountOrderMap = new Map(
|
|
38
|
+
configuredChannels.flatMap((entry) =>
|
|
39
|
+
(Array.isArray(entry?.accounts) ? entry.accounts : []).map(
|
|
40
|
+
(account, accountIndex) => [
|
|
41
|
+
`${String(entry?.channel || "").trim()}:${String(account?.id || "").trim() || "default"}`,
|
|
42
|
+
accountIndex,
|
|
43
|
+
],
|
|
44
|
+
),
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
const channelIds = Array.from(
|
|
48
|
+
new Set([
|
|
49
|
+
...configuredChannels.map((entry) => String(entry.channel || "").trim()),
|
|
50
|
+
]),
|
|
51
|
+
).filter(Boolean);
|
|
52
|
+
|
|
53
|
+
return channelIds
|
|
54
|
+
.flatMap((channelId) => {
|
|
55
|
+
const configuredChannel = configuredChannelMap.get(channelId);
|
|
56
|
+
const statusInfo = channelStatus?.[channelId] || null;
|
|
57
|
+
const accounts = Array.isArray(configuredChannel?.accounts)
|
|
58
|
+
? configuredChannel.accounts
|
|
59
|
+
: [];
|
|
60
|
+
|
|
61
|
+
if (!configuredChannel && !statusInfo) return [];
|
|
62
|
+
|
|
63
|
+
return accounts.map((account) => {
|
|
64
|
+
const accountId = String(account?.id || "").trim() || "default";
|
|
65
|
+
const boundAgentId = String(account?.boundAgentId || "").trim();
|
|
66
|
+
const accountStatusInfo = getResolvedAccountStatusInfo({
|
|
67
|
+
account,
|
|
68
|
+
statusInfo,
|
|
69
|
+
accountId,
|
|
70
|
+
});
|
|
71
|
+
const isImplicitDefaultOwned =
|
|
72
|
+
isDefaultAgent &&
|
|
73
|
+
isImplicitDefaultAccount({ accountId, boundAgentId });
|
|
74
|
+
const isOwned = boundAgentId === agentId || isImplicitDefaultOwned;
|
|
75
|
+
const isImplicitDefaultElsewhere =
|
|
76
|
+
!isDefaultAgent &&
|
|
77
|
+
isImplicitDefaultAccount({ accountId, boundAgentId });
|
|
78
|
+
const isAvailable = canAgentBindAccount({
|
|
79
|
+
accountId,
|
|
80
|
+
boundAgentId,
|
|
81
|
+
agentId,
|
|
82
|
+
isDefaultAgent,
|
|
83
|
+
});
|
|
84
|
+
const ownerAgentId =
|
|
85
|
+
boundAgentId ||
|
|
86
|
+
(isImplicitDefaultAccount({ accountId, boundAgentId })
|
|
87
|
+
? defaultAgentId
|
|
88
|
+
: "");
|
|
89
|
+
const ownerAgentName = String(
|
|
90
|
+
agentNameMap.get(ownerAgentId) || ownerAgentId || "",
|
|
91
|
+
).trim();
|
|
92
|
+
const canNavigateToOwnerAgent =
|
|
93
|
+
!!ownerAgentId && ownerAgentId !== agentId && !!ownerAgentName;
|
|
94
|
+
const canOpenWorkspace =
|
|
95
|
+
channelId === "telegram" &&
|
|
96
|
+
isOwned &&
|
|
97
|
+
accountStatusInfo?.status === "paired";
|
|
98
|
+
|
|
99
|
+
const accountData = {
|
|
100
|
+
id: accountId,
|
|
101
|
+
provider: channelId,
|
|
102
|
+
name: resolveChannelAccountLabel({ channelId, account }),
|
|
103
|
+
rawName: String(account?.name || "").trim(),
|
|
104
|
+
ownerAgentId,
|
|
105
|
+
ownerAgentName,
|
|
106
|
+
boundAgentId,
|
|
107
|
+
isOwned,
|
|
108
|
+
envKey: String(account?.envKey || "").trim(),
|
|
109
|
+
token: String(account?.token || "").trim(),
|
|
110
|
+
isAvailable,
|
|
111
|
+
isBoundElsewhere:
|
|
112
|
+
!isOwned &&
|
|
113
|
+
(!isAvailable || isImplicitDefaultElsewhere || !!ownerAgentId),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
id: `${channelId}:${accountId}`,
|
|
118
|
+
channel: channelId,
|
|
119
|
+
accountId,
|
|
120
|
+
channelOrder: Number(channelOrderMap.get(channelId) ?? 9999),
|
|
121
|
+
accountOrder: Number(
|
|
122
|
+
accountOrderMap.get(`${channelId}:${accountId}`) ?? 9999,
|
|
123
|
+
),
|
|
124
|
+
label: resolveChannelAccountLabel({ channelId, account }),
|
|
125
|
+
isAwaitingPairing: accountStatusInfo?.status !== "paired",
|
|
126
|
+
canOpenWorkspace,
|
|
127
|
+
canNavigateToOwnerAgent,
|
|
128
|
+
ownerAgentId,
|
|
129
|
+
ownerAgentName,
|
|
130
|
+
accountStatusInfo,
|
|
131
|
+
accountData,
|
|
132
|
+
isOwned,
|
|
133
|
+
isAvailable,
|
|
134
|
+
dimmedLabel: accountData.isBoundElsewhere,
|
|
135
|
+
isBoundElsewhere: accountData.isBoundElsewhere,
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
})
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.sort((a, b) => {
|
|
141
|
+
const rankDiff = getChannelItemSortRank(a) - getChannelItemSortRank(b);
|
|
142
|
+
if (rankDiff !== 0) return rankDiff;
|
|
143
|
+
const channelOrderDiff =
|
|
144
|
+
Number(a?.channelOrder ?? 9999) - Number(b?.channelOrder ?? 9999);
|
|
145
|
+
if (channelOrderDiff !== 0) return channelOrderDiff;
|
|
146
|
+
const accountOrderDiff =
|
|
147
|
+
Number(a?.accountOrder ?? 9999) - Number(b?.accountOrder ?? 9999);
|
|
148
|
+
if (accountOrderDiff !== 0) return accountOrderDiff;
|
|
149
|
+
return String(a?.label || "").localeCompare(String(b?.label || ""));
|
|
150
|
+
});
|
|
151
|
+
}, [
|
|
152
|
+
agentId,
|
|
153
|
+
agentNameMap,
|
|
154
|
+
channelStatus,
|
|
155
|
+
configuredChannelMap,
|
|
156
|
+
configuredChannels,
|
|
157
|
+
defaultAgentId,
|
|
158
|
+
isDefaultAgent,
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
const visibleChannelItems = channelItemData.filter(
|
|
162
|
+
(item) => !item?.isBoundElsewhere,
|
|
163
|
+
);
|
|
164
|
+
const assignedElsewhereItems = channelItemData.filter(
|
|
165
|
+
(item) => !!item?.isBoundElsewhere,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (assignedElsewhereItems.length === 0) {
|
|
170
|
+
setShowAssignedElsewhere(false);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (visibleChannelItems.length === 0) {
|
|
174
|
+
setShowAssignedElsewhere(true);
|
|
175
|
+
}
|
|
176
|
+
}, [agentId, assignedElsewhereItems.length, visibleChannelItems.length]);
|
|
177
|
+
|
|
178
|
+
const mergedChannelItems = useMemo(() => {
|
|
179
|
+
const baseItems = [...visibleChannelItems];
|
|
180
|
+
if (assignedElsewhereItems.length === 0) return baseItems;
|
|
181
|
+
baseItems.push({
|
|
182
|
+
id: "__assigned_elsewhere_toggle",
|
|
183
|
+
label: html`
|
|
184
|
+
<span class="inline-flex items-center gap-1.5">
|
|
185
|
+
<span class=${`arrow inline-block ${showAssignedElsewhere ? "" : "-rotate-90"}`}>▼</span>
|
|
186
|
+
<span>Assigned elsewhere</span>
|
|
187
|
+
</span>
|
|
188
|
+
`,
|
|
189
|
+
labelClassName: "text-xs",
|
|
190
|
+
clickable: true,
|
|
191
|
+
onClick: () => setShowAssignedElsewhere((current) => !current),
|
|
192
|
+
dimmedLabel: true,
|
|
193
|
+
trailing: html`
|
|
194
|
+
<span class="inline-flex items-center gap-1.5 text-gray-500">
|
|
195
|
+
<span class="text-[11px] px-2 py-0.5 rounded-full border border-border">
|
|
196
|
+
${assignedElsewhereItems.length}
|
|
197
|
+
</span>
|
|
198
|
+
</span>
|
|
199
|
+
`,
|
|
200
|
+
});
|
|
201
|
+
if (showAssignedElsewhere) {
|
|
202
|
+
baseItems.push(...assignedElsewhereItems);
|
|
203
|
+
}
|
|
204
|
+
return baseItems;
|
|
205
|
+
}, [assignedElsewhereItems, showAssignedElsewhere, visibleChannelItems]);
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
hasDiscordAccount,
|
|
209
|
+
mergedChannelItems,
|
|
210
|
+
};
|
|
211
|
+
};
|
|
@@ -40,6 +40,7 @@ export const AgentPairingSection = ({ agent = {} }) => {
|
|
|
40
40
|
const [loadingBindings, setLoadingBindings] = useState(true);
|
|
41
41
|
const [pairingStatusRefreshing, setPairingStatusRefreshing] = useState(false);
|
|
42
42
|
const pairingRefreshTimerRef = useRef(null);
|
|
43
|
+
const pairingDelayedRefreshTimerRefs = useRef([]);
|
|
43
44
|
const agentId = String(agent?.id || "").trim();
|
|
44
45
|
const isDefaultAgent = !!agent?.default;
|
|
45
46
|
|
|
@@ -82,6 +83,10 @@ export const AgentPairingSection = ({ agent = {} }) => {
|
|
|
82
83
|
if (pairingRefreshTimerRef.current) {
|
|
83
84
|
clearTimeout(pairingRefreshTimerRef.current);
|
|
84
85
|
}
|
|
86
|
+
for (const timerId of pairingDelayedRefreshTimerRefs.current) {
|
|
87
|
+
clearTimeout(timerId);
|
|
88
|
+
}
|
|
89
|
+
pairingDelayedRefreshTimerRefs.current = [];
|
|
85
90
|
},
|
|
86
91
|
[],
|
|
87
92
|
);
|
|
@@ -217,19 +222,27 @@ export const AgentPairingSection = ({ agent = {} }) => {
|
|
|
217
222
|
setPairingStatusRefreshing(false);
|
|
218
223
|
pairingRefreshTimerRef.current = null;
|
|
219
224
|
}, 2800);
|
|
225
|
+
for (const timerId of pairingDelayedRefreshTimerRefs.current) {
|
|
226
|
+
clearTimeout(timerId);
|
|
227
|
+
}
|
|
228
|
+
pairingDelayedRefreshTimerRefs.current = [];
|
|
220
229
|
const refresh = () => {
|
|
221
230
|
pairingsPoll.refresh();
|
|
222
231
|
loadBindings();
|
|
223
232
|
announcePairingsChanged(agentId);
|
|
224
233
|
};
|
|
225
234
|
refresh();
|
|
226
|
-
setTimeout(refresh, 500);
|
|
227
|
-
setTimeout(refresh, 2000);
|
|
235
|
+
pairingDelayedRefreshTimerRefs.current.push(setTimeout(refresh, 500));
|
|
236
|
+
pairingDelayedRefreshTimerRefs.current.push(setTimeout(refresh, 2000));
|
|
228
237
|
}, [agentId, loadBindings, pairingsPoll]);
|
|
229
238
|
|
|
230
239
|
const handleApprove = async (id, channel, accountId = "") => {
|
|
231
|
-
|
|
232
|
-
|
|
240
|
+
try {
|
|
241
|
+
await approvePairing(id, channel, accountId);
|
|
242
|
+
refreshAfterPairingAction();
|
|
243
|
+
} catch (err) {
|
|
244
|
+
showToast(err.message || "Could not approve pairing", "error");
|
|
245
|
+
}
|
|
233
246
|
};
|
|
234
247
|
|
|
235
248
|
const handleReject = async (id, channel, accountId = "") => {
|
|
@@ -63,9 +63,19 @@ export const CreateChannelModal = ({
|
|
|
63
63
|
? initialProvider
|
|
64
64
|
: ALL_CHANNELS[0] || "telegram";
|
|
65
65
|
const providerLabel = getChannelMeta(nextProvider).label || "Channel";
|
|
66
|
+
const nextSelectedChannel =
|
|
67
|
+
existingChannels.find(
|
|
68
|
+
(entry) =>
|
|
69
|
+
String(entry?.channel || "").trim() === String(nextProvider || "").trim(),
|
|
70
|
+
) || null;
|
|
71
|
+
const nextProviderHasAccounts =
|
|
72
|
+
Array.isArray(nextSelectedChannel?.accounts)
|
|
73
|
+
&& nextSelectedChannel.accounts.length > 0;
|
|
66
74
|
const nextName = isEditMode
|
|
67
75
|
? String(account?.name || "").trim() || providerLabel
|
|
68
|
-
:
|
|
76
|
+
: nextProviderHasAccounts
|
|
77
|
+
? ""
|
|
78
|
+
: providerLabel;
|
|
69
79
|
const nextAgentId = isEditMode
|
|
70
80
|
? String(account?.ownerAgentId || "").trim()
|
|
71
81
|
|| String(initialAgentId || "").trim()
|
|
@@ -85,12 +95,16 @@ export const CreateChannelModal = ({
|
|
|
85
95
|
setAgentId(nextAgentId);
|
|
86
96
|
setError("");
|
|
87
97
|
setNameEditedManually(isEditMode);
|
|
88
|
-
}, [
|
|
98
|
+
}, [
|
|
99
|
+
visible,
|
|
100
|
+
initialAgentId,
|
|
101
|
+
initialProvider,
|
|
102
|
+
agents,
|
|
103
|
+
existingChannels,
|
|
104
|
+
isEditMode,
|
|
105
|
+
account,
|
|
106
|
+
]);
|
|
89
107
|
|
|
90
|
-
useEffect(() => {
|
|
91
|
-
if (nameEditedManually) return;
|
|
92
|
-
setName(getChannelMeta(provider).label || "Channel");
|
|
93
|
-
}, [provider, nameEditedManually]);
|
|
94
108
|
const selectedChannel = useMemo(
|
|
95
109
|
() =>
|
|
96
110
|
existingChannels.find(
|
|
@@ -103,6 +117,15 @@ export const CreateChannelModal = ({
|
|
|
103
117
|
() => Array.isArray(selectedChannel?.accounts) && selectedChannel.accounts.length > 0,
|
|
104
118
|
[selectedChannel],
|
|
105
119
|
);
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (nameEditedManually) return;
|
|
122
|
+
const providerLabel = getChannelMeta(provider).label || "Channel";
|
|
123
|
+
if (!isEditMode && providerHasAccounts) {
|
|
124
|
+
setName("");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
setName(providerLabel);
|
|
128
|
+
}, [provider, providerHasAccounts, nameEditedManually, isEditMode]);
|
|
106
129
|
const isSingleAccountProvider = String(provider || "").trim() === "discord";
|
|
107
130
|
|
|
108
131
|
const accountId = useMemo(() => {
|
|
@@ -16,6 +16,10 @@ import {
|
|
|
16
16
|
fetchChannelAccounts,
|
|
17
17
|
updateChannelAccount,
|
|
18
18
|
} from "../lib/api.js";
|
|
19
|
+
import {
|
|
20
|
+
isImplicitDefaultAccount,
|
|
21
|
+
resolveChannelAccountLabel,
|
|
22
|
+
} from "../lib/channel-accounts.js";
|
|
19
23
|
import { createChannelAccountWithProgress } from "../lib/channel-create-operation.js";
|
|
20
24
|
import { CreateChannelModal } from "./agents-tab/create-channel-modal.js";
|
|
21
25
|
import { showToast } from "./toast.js";
|
|
@@ -40,18 +44,6 @@ const getChannelMeta = (channelId = "") => {
|
|
|
40
44
|
);
|
|
41
45
|
};
|
|
42
46
|
|
|
43
|
-
const resolveChannelAccountLabel = ({ channelId, account = {} }) => {
|
|
44
|
-
const providerLabel = getChannelMeta(channelId).label || "Channel";
|
|
45
|
-
const configuredName = String(account?.name || "").trim();
|
|
46
|
-
if (configuredName) return configuredName;
|
|
47
|
-
const accountId = String(account?.id || "").trim();
|
|
48
|
-
if (!accountId || accountId === "default") return providerLabel;
|
|
49
|
-
return `${providerLabel} ${accountId}`;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const isImplicitDefaultAccount = ({ accountId, boundAgentId }) =>
|
|
53
|
-
String(accountId || "").trim() === "default" &&
|
|
54
|
-
!String(boundAgentId || "").trim();
|
|
55
47
|
const announceRestartRequired = () =>
|
|
56
48
|
window.dispatchEvent(new CustomEvent("alphaclaw:restart-required"));
|
|
57
49
|
|
|
@@ -60,6 +52,7 @@ export const ChannelsCard = ({
|
|
|
60
52
|
items = [],
|
|
61
53
|
loadingLabel = "Loading...",
|
|
62
54
|
actions = null,
|
|
55
|
+
renderItem = null,
|
|
63
56
|
}) => html`
|
|
64
57
|
<div class="bg-surface border border-border rounded-xl p-4">
|
|
65
58
|
<div class="flex items-center justify-between gap-3 mb-3">
|
|
@@ -71,6 +64,10 @@ export const ChannelsCard = ({
|
|
|
71
64
|
? items.map((item) => {
|
|
72
65
|
const channelMeta = getChannelMeta(item.channel || item.id);
|
|
73
66
|
const clickable = !!item.clickable;
|
|
67
|
+
const customItem = renderItem
|
|
68
|
+
? renderItem({ item, channelMeta, clickable })
|
|
69
|
+
: null;
|
|
70
|
+
if (customItem) return customItem;
|
|
74
71
|
return html`
|
|
75
72
|
<div
|
|
76
73
|
key=${item.id || item.channel}
|
|
@@ -342,7 +339,11 @@ export const Channels = ({
|
|
|
342
339
|
const accountData = {
|
|
343
340
|
id: accountId,
|
|
344
341
|
provider: channelId,
|
|
345
|
-
name: resolveChannelAccountLabel({
|
|
342
|
+
name: resolveChannelAccountLabel({
|
|
343
|
+
channelId,
|
|
344
|
+
account,
|
|
345
|
+
providerLabel: getChannelMeta(channelId).label || "Channel",
|
|
346
|
+
}),
|
|
346
347
|
ownerAgentId,
|
|
347
348
|
envKey: String(account?.envKey || "").trim(),
|
|
348
349
|
token: String(account?.token || "").trim(),
|
|
@@ -422,7 +423,11 @@ export const Channels = ({
|
|
|
422
423
|
accountOrder: Number(
|
|
423
424
|
accountOrderMap.get(`${channelId}:${accountId}`) ?? 9999,
|
|
424
425
|
),
|
|
425
|
-
label: resolveChannelAccountLabel({
|
|
426
|
+
label: resolveChannelAccountLabel({
|
|
427
|
+
channelId,
|
|
428
|
+
account,
|
|
429
|
+
providerLabel: getChannelMeta(channelId).label || "Channel",
|
|
430
|
+
}),
|
|
426
431
|
isAwaitingPairing: accountStatus !== "paired",
|
|
427
432
|
detailText: isClickable ? "Workspace" : "",
|
|
428
433
|
detailChevron: isClickable,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const resolveChannelAccountLabel = ({
|
|
2
|
+
channelId,
|
|
3
|
+
account = {},
|
|
4
|
+
providerLabel = "",
|
|
5
|
+
}) => {
|
|
6
|
+
const fallbackProviderLabel = channelId
|
|
7
|
+
? channelId.charAt(0).toUpperCase() + channelId.slice(1)
|
|
8
|
+
: "Channel";
|
|
9
|
+
const resolvedProviderLabel = String(providerLabel || "").trim()
|
|
10
|
+
|| fallbackProviderLabel;
|
|
11
|
+
const configuredName = String(account?.name || "").trim();
|
|
12
|
+
if (configuredName) return configuredName;
|
|
13
|
+
const accountId = String(account?.id || "").trim();
|
|
14
|
+
if (!accountId || accountId === "default") return resolvedProviderLabel;
|
|
15
|
+
return `${resolvedProviderLabel} ${accountId}`;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const isImplicitDefaultAccount = ({ accountId, boundAgentId }) =>
|
|
19
|
+
String(accountId || "").trim() === "default" &&
|
|
20
|
+
!String(boundAgentId || "").trim();
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
kDefaultAgentId,
|
|
5
|
+
resolveAgentWorkspacePath,
|
|
6
|
+
loadConfig,
|
|
7
|
+
saveConfig,
|
|
8
|
+
cloneJson,
|
|
9
|
+
getSafeStat,
|
|
10
|
+
calculatePathSizeBytes,
|
|
11
|
+
withNormalizedAgentsConfig,
|
|
12
|
+
isValidAgentId,
|
|
13
|
+
resolveRequestedWorkspacePath,
|
|
14
|
+
ensureAgentScaffold,
|
|
15
|
+
} = require("./shared");
|
|
16
|
+
|
|
17
|
+
const createAgentsDomain = ({ fsImpl, OPENCLAW_DIR }) => {
|
|
18
|
+
const listAgents = () => {
|
|
19
|
+
const cfg = withNormalizedAgentsConfig({
|
|
20
|
+
OPENCLAW_DIR,
|
|
21
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
22
|
+
});
|
|
23
|
+
return (cfg.agents?.list || []).map((entry) => ({
|
|
24
|
+
...entry,
|
|
25
|
+
id: String(entry.id || "").trim(),
|
|
26
|
+
name: String(entry.name || "").trim() || String(entry.id || "").trim(),
|
|
27
|
+
default: !!entry.default,
|
|
28
|
+
}));
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const getAgent = (agentId) => {
|
|
32
|
+
const normalized = String(agentId || "").trim();
|
|
33
|
+
return listAgents().find((entry) => entry.id === normalized) || null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const getAgentWorkspaceSize = (agentId) => {
|
|
37
|
+
const normalized = String(agentId || "").trim();
|
|
38
|
+
const agent = getAgent(normalized);
|
|
39
|
+
if (!agent) throw new Error(`Agent "${normalized}" not found`);
|
|
40
|
+
const workspacePath = String(
|
|
41
|
+
agent.workspace ||
|
|
42
|
+
resolveAgentWorkspacePath({ OPENCLAW_DIR, agentId: normalized }),
|
|
43
|
+
).trim();
|
|
44
|
+
if (!workspacePath) {
|
|
45
|
+
return { workspacePath: "", exists: false, sizeBytes: 0 };
|
|
46
|
+
}
|
|
47
|
+
const stat = getSafeStat({ fsImpl, targetPath: workspacePath });
|
|
48
|
+
if (!stat) {
|
|
49
|
+
return { workspacePath, exists: false, sizeBytes: 0 };
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
workspacePath,
|
|
53
|
+
exists: true,
|
|
54
|
+
sizeBytes: calculatePathSizeBytes({ fsImpl, targetPath: workspacePath }),
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const createAgent = (input = {}) => {
|
|
59
|
+
const agentId = String(input.id || "").trim();
|
|
60
|
+
if (!isValidAgentId(agentId)) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
"Agent id must be lowercase letters, numbers, and hyphens only",
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const cfg = withNormalizedAgentsConfig({
|
|
67
|
+
OPENCLAW_DIR,
|
|
68
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
69
|
+
});
|
|
70
|
+
const existing = cfg.agents.list.find((entry) => entry.id === agentId);
|
|
71
|
+
if (existing) {
|
|
72
|
+
throw new Error(`Agent "${agentId}" already exists`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const workspacePath = resolveRequestedWorkspacePath({
|
|
76
|
+
OPENCLAW_DIR,
|
|
77
|
+
agentId,
|
|
78
|
+
workspaceFolder: input.workspaceFolder,
|
|
79
|
+
});
|
|
80
|
+
const { workspacePath: scaffoldWorkspacePath, agentDirPath } =
|
|
81
|
+
ensureAgentScaffold({
|
|
82
|
+
fsImpl,
|
|
83
|
+
workspacePath,
|
|
84
|
+
OPENCLAW_DIR,
|
|
85
|
+
agentId,
|
|
86
|
+
});
|
|
87
|
+
const nextAgent = {
|
|
88
|
+
id: agentId,
|
|
89
|
+
name: String(input.name || "").trim() || agentId,
|
|
90
|
+
default: false,
|
|
91
|
+
workspace: scaffoldWorkspacePath,
|
|
92
|
+
agentDir: agentDirPath,
|
|
93
|
+
...(input.model ? { model: input.model } : {}),
|
|
94
|
+
...(input.identity && typeof input.identity === "object"
|
|
95
|
+
? { identity: { ...input.identity } }
|
|
96
|
+
: {}),
|
|
97
|
+
};
|
|
98
|
+
cfg.agents.list = [...cfg.agents.list, nextAgent];
|
|
99
|
+
saveConfig({ fsImpl, OPENCLAW_DIR, config: cfg });
|
|
100
|
+
return nextAgent;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const updateAgent = (agentId, patch = {}) => {
|
|
104
|
+
const normalized = String(agentId || "").trim();
|
|
105
|
+
const cfg = withNormalizedAgentsConfig({
|
|
106
|
+
OPENCLAW_DIR,
|
|
107
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
108
|
+
});
|
|
109
|
+
const index = cfg.agents.list.findIndex((entry) => entry.id === normalized);
|
|
110
|
+
if (index < 0) throw new Error(`Agent "${normalized}" not found`);
|
|
111
|
+
const current = cfg.agents.list[index];
|
|
112
|
+
const next = {
|
|
113
|
+
...current,
|
|
114
|
+
...(patch.name !== undefined
|
|
115
|
+
? { name: String(patch.name || "").trim() }
|
|
116
|
+
: {}),
|
|
117
|
+
...(patch.identity !== undefined
|
|
118
|
+
? {
|
|
119
|
+
identity:
|
|
120
|
+
patch.identity && typeof patch.identity === "object"
|
|
121
|
+
? { ...patch.identity }
|
|
122
|
+
: {},
|
|
123
|
+
}
|
|
124
|
+
: {}),
|
|
125
|
+
};
|
|
126
|
+
if (patch.model !== undefined) {
|
|
127
|
+
if (patch.model === null) {
|
|
128
|
+
delete next.model;
|
|
129
|
+
} else {
|
|
130
|
+
next.model = patch.model;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!String(next.name || "").trim()) next.name = normalized;
|
|
134
|
+
cfg.agents.list[index] = next;
|
|
135
|
+
saveConfig({ fsImpl, OPENCLAW_DIR, config: cfg });
|
|
136
|
+
return next;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const setDefaultAgent = (agentId) => {
|
|
140
|
+
const normalized = String(agentId || "").trim();
|
|
141
|
+
const cfg = withNormalizedAgentsConfig({
|
|
142
|
+
OPENCLAW_DIR,
|
|
143
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
144
|
+
});
|
|
145
|
+
const exists = cfg.agents.list.some((entry) => entry.id === normalized);
|
|
146
|
+
if (!exists) throw new Error(`Agent "${normalized}" not found`);
|
|
147
|
+
cfg.agents.list = cfg.agents.list.map((entry) => ({
|
|
148
|
+
...entry,
|
|
149
|
+
default: entry.id === normalized,
|
|
150
|
+
}));
|
|
151
|
+
saveConfig({ fsImpl, OPENCLAW_DIR, config: cfg });
|
|
152
|
+
return cfg.agents.list.find((entry) => entry.id === normalized) || null;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const deleteAgent = (agentId, { keepWorkspace = true } = {}) => {
|
|
156
|
+
const normalized = String(agentId || "").trim();
|
|
157
|
+
if (!normalized || normalized === kDefaultAgentId) {
|
|
158
|
+
throw new Error("The default main agent cannot be deleted");
|
|
159
|
+
}
|
|
160
|
+
const cfg = withNormalizedAgentsConfig({
|
|
161
|
+
OPENCLAW_DIR,
|
|
162
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
163
|
+
});
|
|
164
|
+
const target = cfg.agents.list.find((entry) => entry.id === normalized);
|
|
165
|
+
if (!target) throw new Error(`Agent "${normalized}" not found`);
|
|
166
|
+
if (target.default) {
|
|
167
|
+
throw new Error("Default agent cannot be deleted");
|
|
168
|
+
}
|
|
169
|
+
cfg.agents.list = cfg.agents.list.filter(
|
|
170
|
+
(entry) => entry.id !== normalized,
|
|
171
|
+
);
|
|
172
|
+
if (Array.isArray(cfg.bindings)) {
|
|
173
|
+
cfg.bindings = cfg.bindings.filter(
|
|
174
|
+
(binding) => String(binding?.agentId || "") !== normalized,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
saveConfig({ fsImpl, OPENCLAW_DIR, config: cfg });
|
|
178
|
+
|
|
179
|
+
if (!keepWorkspace) {
|
|
180
|
+
const workspacePath = String(
|
|
181
|
+
target.workspace ||
|
|
182
|
+
resolveAgentWorkspacePath({
|
|
183
|
+
OPENCLAW_DIR,
|
|
184
|
+
agentId: normalized,
|
|
185
|
+
}),
|
|
186
|
+
).trim();
|
|
187
|
+
const agentDirPath = path.join(OPENCLAW_DIR, "agents", normalized);
|
|
188
|
+
if (workspacePath) {
|
|
189
|
+
fsImpl.rmSync(workspacePath, { recursive: true, force: true });
|
|
190
|
+
}
|
|
191
|
+
fsImpl.rmSync(agentDirPath, { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
return { ok: true };
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
listAgents,
|
|
198
|
+
getAgent,
|
|
199
|
+
getAgentWorkspaceSize,
|
|
200
|
+
createAgent,
|
|
201
|
+
updateAgent,
|
|
202
|
+
setDefaultAgent,
|
|
203
|
+
deleteAgent,
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
module.exports = { createAgentsDomain };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const {
|
|
2
|
+
loadConfig,
|
|
3
|
+
saveConfig,
|
|
4
|
+
cloneJson,
|
|
5
|
+
normalizeBindingMatch,
|
|
6
|
+
matchesBinding,
|
|
7
|
+
appendBindingToConfig,
|
|
8
|
+
withNormalizedAgentsConfig,
|
|
9
|
+
} = require("./shared");
|
|
10
|
+
|
|
11
|
+
const createBindingsDomain = ({ fsImpl, OPENCLAW_DIR }) => {
|
|
12
|
+
const getBindingsForAgent = (agentId) => {
|
|
13
|
+
const normalized = String(agentId || "").trim();
|
|
14
|
+
const cfg = withNormalizedAgentsConfig({
|
|
15
|
+
OPENCLAW_DIR,
|
|
16
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
17
|
+
});
|
|
18
|
+
const bindings = Array.isArray(cfg.bindings) ? cfg.bindings : [];
|
|
19
|
+
return bindings
|
|
20
|
+
.filter((binding) => String(binding?.agentId || "").trim() === normalized)
|
|
21
|
+
.map((binding) => cloneJson(binding));
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const addBinding = (agentId, input = {}) => {
|
|
25
|
+
const normalizedAgentId = String(agentId || "").trim();
|
|
26
|
+
const cfg = withNormalizedAgentsConfig({
|
|
27
|
+
OPENCLAW_DIR,
|
|
28
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
29
|
+
});
|
|
30
|
+
const agent = cfg.agents.list.find(
|
|
31
|
+
(entry) => entry.id === normalizedAgentId,
|
|
32
|
+
);
|
|
33
|
+
if (!agent) throw new Error(`Agent "${normalizedAgentId}" not found`);
|
|
34
|
+
const match = normalizeBindingMatch(input);
|
|
35
|
+
const nextBinding = appendBindingToConfig({
|
|
36
|
+
cfg,
|
|
37
|
+
agentId: normalizedAgentId,
|
|
38
|
+
match,
|
|
39
|
+
});
|
|
40
|
+
saveConfig({ fsImpl, OPENCLAW_DIR, config: cfg });
|
|
41
|
+
return nextBinding;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const removeBinding = (agentId, input = {}) => {
|
|
45
|
+
const normalizedAgentId = String(agentId || "").trim();
|
|
46
|
+
const cfg = withNormalizedAgentsConfig({
|
|
47
|
+
OPENCLAW_DIR,
|
|
48
|
+
cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
|
|
49
|
+
});
|
|
50
|
+
const bindings = Array.isArray(cfg.bindings) ? cfg.bindings : [];
|
|
51
|
+
const nextMatch = normalizeBindingMatch(input);
|
|
52
|
+
const nextBindings = bindings.filter(
|
|
53
|
+
(binding) =>
|
|
54
|
+
!(
|
|
55
|
+
String(binding?.agentId || "").trim() === normalizedAgentId &&
|
|
56
|
+
matchesBinding(binding?.match || {}, nextMatch)
|
|
57
|
+
),
|
|
58
|
+
);
|
|
59
|
+
if (nextBindings.length === bindings.length) {
|
|
60
|
+
throw new Error("Binding not found");
|
|
61
|
+
}
|
|
62
|
+
cfg.bindings = nextBindings;
|
|
63
|
+
saveConfig({ fsImpl, OPENCLAW_DIR, config: cfg });
|
|
64
|
+
return { ok: true };
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
getBindingsForAgent,
|
|
69
|
+
addBinding,
|
|
70
|
+
removeBinding,
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
module.exports = { createBindingsDomain };
|