@chrysb/alphaclaw 0.5.5 → 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 +65 -39
- 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-serve.js +2 -12
- 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 +74 -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/startup.js +23 -0
- package/lib/server/telegram-workspace.js +61 -10
- package/lib/server/topic-registry.js +66 -7
- package/lib/server/watchdog.js +151 -27
- package/lib/server/webhooks.js +60 -12
- package/lib/server.js +40 -27
- 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,266 @@
|
|
|
1
|
+
const parseKeepWorkspace = (value) => {
|
|
2
|
+
if (value === undefined || value === null) return true;
|
|
3
|
+
const normalized = String(value).trim().toLowerCase();
|
|
4
|
+
if (!normalized) return true;
|
|
5
|
+
return !["0", "false", "no", "off"].includes(normalized);
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const registerAgentRoutes = ({
|
|
9
|
+
app,
|
|
10
|
+
agentsService,
|
|
11
|
+
restartRequiredState = null,
|
|
12
|
+
operationEvents = null,
|
|
13
|
+
}) => {
|
|
14
|
+
app.get("/api/channels/accounts", (_req, res) => {
|
|
15
|
+
try {
|
|
16
|
+
res.json({
|
|
17
|
+
ok: true,
|
|
18
|
+
channels: agentsService.listConfiguredChannelAccounts(),
|
|
19
|
+
});
|
|
20
|
+
} catch (error) {
|
|
21
|
+
res.status(500).json({ ok: false, error: error.message });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
app.get("/api/channels/accounts/token", (req, res) => {
|
|
26
|
+
try {
|
|
27
|
+
const provider = String(req.query?.provider || "").trim();
|
|
28
|
+
const accountId = String(req.query?.accountId || "").trim() || "default";
|
|
29
|
+
const result = agentsService.getChannelAccountToken({
|
|
30
|
+
provider,
|
|
31
|
+
accountId,
|
|
32
|
+
});
|
|
33
|
+
return res.json({ ok: true, ...result });
|
|
34
|
+
} catch (error) {
|
|
35
|
+
const status = String(error.message || "").includes("not found")
|
|
36
|
+
? 404
|
|
37
|
+
: 400;
|
|
38
|
+
return res.status(status).json({ ok: false, error: error.message });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
app.post("/api/channels/accounts", async (req, res) => {
|
|
43
|
+
try {
|
|
44
|
+
const body = req.body || {};
|
|
45
|
+
const result = await agentsService.createChannelAccount(body);
|
|
46
|
+
return res.status(201).json({ ok: true, ...result });
|
|
47
|
+
} catch (error) {
|
|
48
|
+
const message = String(error.message || "");
|
|
49
|
+
const status = message.includes("already exists")
|
|
50
|
+
? 409
|
|
51
|
+
: message.includes("already assigned")
|
|
52
|
+
? 409
|
|
53
|
+
: message.includes("not found")
|
|
54
|
+
? 404
|
|
55
|
+
: 400;
|
|
56
|
+
return res.status(status).json({ ok: false, error: error.message });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
app.post("/api/channels/accounts/jobs", (req, res) => {
|
|
61
|
+
if (!operationEvents?.createOperation) {
|
|
62
|
+
return res
|
|
63
|
+
.status(503)
|
|
64
|
+
.json({ ok: false, error: "Operation events unavailable" });
|
|
65
|
+
}
|
|
66
|
+
const body = req.body || {};
|
|
67
|
+
const { operationId } = operationEvents.createOperation({
|
|
68
|
+
type: "channel-account-create",
|
|
69
|
+
});
|
|
70
|
+
(async () => {
|
|
71
|
+
try {
|
|
72
|
+
const result = await agentsService.createChannelAccount(body, {
|
|
73
|
+
onProgress: ({ phase = "", label = "" } = {}) => {
|
|
74
|
+
operationEvents.publish(operationId, {
|
|
75
|
+
event: "phase",
|
|
76
|
+
data: {
|
|
77
|
+
phase: String(phase || "").trim(),
|
|
78
|
+
label: String(label || "").trim(),
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
operationEvents.complete(operationId, { ok: true, ...result });
|
|
84
|
+
} catch (error) {
|
|
85
|
+
operationEvents.fail(operationId, error);
|
|
86
|
+
}
|
|
87
|
+
})();
|
|
88
|
+
return res.status(202).json({
|
|
89
|
+
ok: true,
|
|
90
|
+
operationId,
|
|
91
|
+
streamUrl: `/api/operations/${encodeURIComponent(operationId)}/events`,
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
app.get("/api/operations/:operationId/events", (req, res) => {
|
|
96
|
+
if (!operationEvents?.subscribe) {
|
|
97
|
+
return res
|
|
98
|
+
.status(503)
|
|
99
|
+
.json({ ok: false, error: "Operation events unavailable" });
|
|
100
|
+
}
|
|
101
|
+
const subscribed = operationEvents.subscribe({
|
|
102
|
+
operationId: req.params.operationId,
|
|
103
|
+
req,
|
|
104
|
+
res,
|
|
105
|
+
});
|
|
106
|
+
if (!subscribed) {
|
|
107
|
+
return res.status(404).json({ ok: false, error: "Operation not found" });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
app.put("/api/channels/accounts", (req, res) => {
|
|
112
|
+
try {
|
|
113
|
+
const result = agentsService.updateChannelAccount(req.body || {});
|
|
114
|
+
const restartRequired = !!result?.tokenUpdated;
|
|
115
|
+
if (restartRequired) {
|
|
116
|
+
restartRequiredState?.markRequired?.("channel_token_updated");
|
|
117
|
+
}
|
|
118
|
+
return res.json({ ok: true, restartRequired, ...result });
|
|
119
|
+
} catch (error) {
|
|
120
|
+
const status = String(error.message || "").includes("not found")
|
|
121
|
+
? 404
|
|
122
|
+
: 400;
|
|
123
|
+
return res.status(status).json({ ok: false, error: error.message });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
app.delete("/api/channels/accounts", async (req, res) => {
|
|
128
|
+
try {
|
|
129
|
+
const body = req.body || {};
|
|
130
|
+
await agentsService.deleteChannelAccount(body);
|
|
131
|
+
return res.json({ ok: true });
|
|
132
|
+
} catch (error) {
|
|
133
|
+
const status = String(error.message || "").includes("not found")
|
|
134
|
+
? 404
|
|
135
|
+
: 400;
|
|
136
|
+
return res.status(status).json({ ok: false, error: error.message });
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
app.get("/api/agents", (_req, res) => {
|
|
141
|
+
try {
|
|
142
|
+
res.json({ ok: true, agents: agentsService.listAgents() });
|
|
143
|
+
} catch (error) {
|
|
144
|
+
res.status(500).json({ ok: false, error: error.message });
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
app.get("/api/agents/:id", (req, res) => {
|
|
149
|
+
try {
|
|
150
|
+
const agent = agentsService.getAgent(req.params.id);
|
|
151
|
+
if (!agent)
|
|
152
|
+
return res.status(404).json({ ok: false, error: "Agent not found" });
|
|
153
|
+
return res.json({ ok: true, agent });
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return res.status(500).json({ ok: false, error: error.message });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
app.get("/api/agents/:id/workspace-size", (req, res) => {
|
|
160
|
+
try {
|
|
161
|
+
const workspace = agentsService.getAgentWorkspaceSize(req.params.id);
|
|
162
|
+
return res.json({ ok: true, ...workspace });
|
|
163
|
+
} catch (error) {
|
|
164
|
+
const status = String(error.message || "").includes("not found")
|
|
165
|
+
? 404
|
|
166
|
+
: 500;
|
|
167
|
+
return res.status(status).json({ ok: false, error: error.message });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
app.get("/api/agents/:id/bindings", (req, res) => {
|
|
172
|
+
try {
|
|
173
|
+
const agent = agentsService.getAgent(req.params.id);
|
|
174
|
+
if (!agent)
|
|
175
|
+
return res.status(404).json({ ok: false, error: "Agent not found" });
|
|
176
|
+
return res.json({
|
|
177
|
+
ok: true,
|
|
178
|
+
bindings: agentsService.getBindingsForAgent(req.params.id),
|
|
179
|
+
});
|
|
180
|
+
} catch (error) {
|
|
181
|
+
return res.status(500).json({ ok: false, error: error.message });
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
app.post("/api/agents", (req, res) => {
|
|
186
|
+
try {
|
|
187
|
+
const body = req.body || {};
|
|
188
|
+
if (!String(body.id || "").trim()) {
|
|
189
|
+
return res.status(400).json({ ok: false, error: "id is required" });
|
|
190
|
+
}
|
|
191
|
+
const agent = agentsService.createAgent(body);
|
|
192
|
+
return res.status(201).json({ ok: true, agent });
|
|
193
|
+
} catch (error) {
|
|
194
|
+
const status = String(error.message || "").includes("already exists")
|
|
195
|
+
? 409
|
|
196
|
+
: 400;
|
|
197
|
+
return res.status(status).json({ ok: false, error: error.message });
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
app.put("/api/agents/:id", (req, res) => {
|
|
202
|
+
try {
|
|
203
|
+
const agent = agentsService.updateAgent(req.params.id, req.body || {});
|
|
204
|
+
return res.json({ ok: true, agent });
|
|
205
|
+
} catch (error) {
|
|
206
|
+
const status = String(error.message || "").includes("not found")
|
|
207
|
+
? 404
|
|
208
|
+
: 400;
|
|
209
|
+
return res.status(status).json({ ok: false, error: error.message });
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
app.post("/api/agents/:id/bindings", (req, res) => {
|
|
214
|
+
try {
|
|
215
|
+
const binding = agentsService.addBinding(req.params.id, req.body || {});
|
|
216
|
+
return res.status(201).json({ ok: true, binding });
|
|
217
|
+
} catch (error) {
|
|
218
|
+
const message = String(error.message || "");
|
|
219
|
+
const status = message.includes("not found")
|
|
220
|
+
? 404
|
|
221
|
+
: message.includes("already assigned")
|
|
222
|
+
? 409
|
|
223
|
+
: 400;
|
|
224
|
+
return res.status(status).json({ ok: false, error: error.message });
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
app.delete("/api/agents/:id/bindings", (req, res) => {
|
|
229
|
+
try {
|
|
230
|
+
agentsService.removeBinding(req.params.id, req.body || {});
|
|
231
|
+
return res.json({ ok: true });
|
|
232
|
+
} catch (error) {
|
|
233
|
+
const status = String(error.message || "").includes("not found")
|
|
234
|
+
? 404
|
|
235
|
+
: 400;
|
|
236
|
+
return res.status(status).json({ ok: false, error: error.message });
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
app.delete("/api/agents/:id", (req, res) => {
|
|
241
|
+
try {
|
|
242
|
+
const keepWorkspace = parseKeepWorkspace(req.query.keepWorkspace);
|
|
243
|
+
agentsService.deleteAgent(req.params.id, { keepWorkspace });
|
|
244
|
+
return res.json({ ok: true });
|
|
245
|
+
} catch (error) {
|
|
246
|
+
const status = String(error.message || "").includes("not found")
|
|
247
|
+
? 404
|
|
248
|
+
: 400;
|
|
249
|
+
return res.status(status).json({ ok: false, error: error.message });
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
app.post("/api/agents/:id/default", (req, res) => {
|
|
254
|
+
try {
|
|
255
|
+
const agent = agentsService.setDefaultAgent(req.params.id);
|
|
256
|
+
return res.json({ ok: true, agent });
|
|
257
|
+
} catch (error) {
|
|
258
|
+
const status = String(error.message || "").includes("not found")
|
|
259
|
+
? 404
|
|
260
|
+
: 400;
|
|
261
|
+
return res.status(status).json({ ok: false, error: error.message });
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
module.exports = { registerAgentRoutes };
|
|
@@ -1,6 +1,61 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
2
3
|
const { OPENCLAW_DIR } = require("../constants");
|
|
3
4
|
const { buildManagedPaths } = require("../internal-files-migration");
|
|
5
|
+
const { parseJsonObjectFromNoisyOutput } = require("../utils/json");
|
|
6
|
+
|
|
7
|
+
const resolvePairingStorePath = ({ openclawDir, channel }) =>
|
|
8
|
+
path.join(openclawDir, "credentials", `${String(channel).trim().toLowerCase()}-pairing.json`);
|
|
9
|
+
|
|
10
|
+
const readPairingStore = ({ fsModule, filePath }) => {
|
|
11
|
+
try {
|
|
12
|
+
const raw = fsModule.readFileSync(filePath, "utf8");
|
|
13
|
+
const parsed = JSON.parse(raw);
|
|
14
|
+
return Array.isArray(parsed?.requests) ? parsed.requests : [];
|
|
15
|
+
} catch {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const writePairingStore = ({ fsModule, filePath, requests }) => {
|
|
21
|
+
fsModule.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
22
|
+
fsModule.writeFileSync(filePath, JSON.stringify({ version: 1, requests }, null, 2));
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const removeRequestFromPairingStore = ({ fsModule, openclawDir, channel, code, accountId }) => {
|
|
26
|
+
const filePath = resolvePairingStorePath({ openclawDir, channel });
|
|
27
|
+
const requests = readPairingStore({ fsModule, filePath });
|
|
28
|
+
const normalizedCode = String(code || "").trim().toUpperCase();
|
|
29
|
+
const normalizedAccountId = String(accountId || "").trim().toLowerCase();
|
|
30
|
+
const nextRequests = requests.filter((entry) => {
|
|
31
|
+
const entryCode = String(entry?.code || "").trim().toUpperCase();
|
|
32
|
+
if (entryCode !== normalizedCode) return true;
|
|
33
|
+
if (normalizedAccountId) {
|
|
34
|
+
const entryAccountId = String(entry?.meta?.accountId || "").trim().toLowerCase();
|
|
35
|
+
return entryAccountId !== normalizedAccountId;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
});
|
|
39
|
+
if (nextRequests.length !== requests.length) {
|
|
40
|
+
writePairingStore({ fsModule, filePath, requests: nextRequests });
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const removeAccountRequestsFromPairingStore = ({ fsModule, openclawDir, channel, accountId }) => {
|
|
47
|
+
const filePath = resolvePairingStorePath({ openclawDir, channel });
|
|
48
|
+
const requests = readPairingStore({ fsModule, filePath });
|
|
49
|
+
if (requests.length === 0) return;
|
|
50
|
+
const normalizedAccountId = String(accountId || "").trim().toLowerCase() || "default";
|
|
51
|
+
const nextRequests = requests.filter((entry) => {
|
|
52
|
+
const entryAccountId = String(entry?.meta?.accountId || "").trim().toLowerCase() || "default";
|
|
53
|
+
return entryAccountId !== normalizedAccountId;
|
|
54
|
+
});
|
|
55
|
+
if (nextRequests.length !== requests.length) {
|
|
56
|
+
writePairingStore({ fsModule, filePath, requests: nextRequests });
|
|
57
|
+
}
|
|
58
|
+
};
|
|
4
59
|
|
|
5
60
|
const registerPairingRoutes = ({ app, clawCmd, isOnboarded, fsModule = fs, openclawDir = OPENCLAW_DIR }) => {
|
|
6
61
|
let pairingCache = { pending: [], ts: 0 };
|
|
@@ -22,6 +77,29 @@ const registerPairingRoutes = ({ app, clawCmd, isOnboarded, fsModule = fs, openc
|
|
|
22
77
|
);
|
|
23
78
|
};
|
|
24
79
|
|
|
80
|
+
const parsePendingPairings = (stdout, channel) => {
|
|
81
|
+
const parsed = parseJsonObjectFromNoisyOutput(stdout) || {};
|
|
82
|
+
const requestLists = [
|
|
83
|
+
...(Array.isArray(parsed?.requests) ? [parsed.requests] : []),
|
|
84
|
+
...(Array.isArray(parsed?.pending) ? [parsed.pending] : []),
|
|
85
|
+
];
|
|
86
|
+
return requestLists
|
|
87
|
+
.flat()
|
|
88
|
+
.map((entry) => {
|
|
89
|
+
const code = String(entry?.code || entry?.pairingCode || "").trim().toUpperCase();
|
|
90
|
+
if (!code) return null;
|
|
91
|
+
return {
|
|
92
|
+
id: code,
|
|
93
|
+
code,
|
|
94
|
+
channel: String(channel || "").trim(),
|
|
95
|
+
accountId:
|
|
96
|
+
String(entry?.meta?.accountId || entry?.accountId || "").trim() || "default",
|
|
97
|
+
requesterId: String(entry?.id || entry?.requesterId || "").trim(),
|
|
98
|
+
};
|
|
99
|
+
})
|
|
100
|
+
.filter(Boolean);
|
|
101
|
+
};
|
|
102
|
+
|
|
25
103
|
app.get("/api/pairings", async (req, res) => {
|
|
26
104
|
if (Date.now() - pairingCache.ts < PAIRING_CACHE_TTL) {
|
|
27
105
|
return res.json({ pending: pairingCache.pending });
|
|
@@ -32,24 +110,20 @@ const registerPairingRoutes = ({ app, clawCmd, isOnboarded, fsModule = fs, openc
|
|
|
32
110
|
|
|
33
111
|
for (const ch of channels) {
|
|
34
112
|
try {
|
|
35
|
-
const config = JSON.parse(
|
|
113
|
+
const config = JSON.parse(
|
|
114
|
+
fsModule.readFileSync(`${openclawDir}/openclaw.json`, "utf8"),
|
|
115
|
+
);
|
|
36
116
|
if (!config.channels?.[ch]?.enabled) continue;
|
|
37
117
|
} catch {
|
|
38
118
|
continue;
|
|
39
119
|
}
|
|
40
120
|
|
|
41
|
-
const result = await clawCmd(`pairing list ${ch}`, { quiet: true });
|
|
121
|
+
const result = await clawCmd(`pairing list --channel ${ch} --json`, { quiet: true });
|
|
42
122
|
if (result.ok && result.stdout) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
pending.push({
|
|
48
|
-
id: codeMatch[1],
|
|
49
|
-
code: codeMatch[1],
|
|
50
|
-
channel: ch,
|
|
51
|
-
});
|
|
52
|
-
}
|
|
123
|
+
try {
|
|
124
|
+
pending.push(...parsePendingPairings(result.stdout, ch));
|
|
125
|
+
} catch {
|
|
126
|
+
// Ignore malformed output for a single channel and keep the rest of the response.
|
|
53
127
|
}
|
|
54
128
|
}
|
|
55
129
|
}
|
|
@@ -60,26 +134,58 @@ const registerPairingRoutes = ({ app, clawCmd, isOnboarded, fsModule = fs, openc
|
|
|
60
134
|
|
|
61
135
|
app.post("/api/pairings/:id/approve", async (req, res) => {
|
|
62
136
|
const channel = req.body.channel || "telegram";
|
|
63
|
-
const
|
|
137
|
+
const accountId = String(req.body?.accountId || "").trim();
|
|
138
|
+
const approveCmd = accountId
|
|
139
|
+
? `pairing approve --channel ${channel} --account ${accountId} ${req.params.id}`
|
|
140
|
+
: `pairing approve ${channel} ${req.params.id}`;
|
|
141
|
+
const result = await clawCmd(approveCmd);
|
|
64
142
|
res.json(result);
|
|
65
143
|
});
|
|
66
144
|
|
|
67
|
-
app.post("/api/pairings/:id/reject",
|
|
68
|
-
const channel = req.body.channel || "telegram";
|
|
69
|
-
const
|
|
70
|
-
|
|
145
|
+
app.post("/api/pairings/:id/reject", (req, res) => {
|
|
146
|
+
const channel = String(req.body.channel || "telegram").trim();
|
|
147
|
+
const accountId = String(req.body?.accountId || "").trim();
|
|
148
|
+
try {
|
|
149
|
+
const removed = removeRequestFromPairingStore({
|
|
150
|
+
fsModule,
|
|
151
|
+
openclawDir,
|
|
152
|
+
channel,
|
|
153
|
+
code: req.params.id,
|
|
154
|
+
accountId,
|
|
155
|
+
});
|
|
156
|
+
pairingCache.ts = 0;
|
|
157
|
+
if (removed) {
|
|
158
|
+
console.log(`[alphaclaw] Rejected pairing request ${req.params.id} for ${channel}${accountId ? `/${accountId}` : ""}`);
|
|
159
|
+
return res.json({ ok: true, removed: true });
|
|
160
|
+
}
|
|
161
|
+
return res.status(404).json({
|
|
162
|
+
ok: false,
|
|
163
|
+
removed: false,
|
|
164
|
+
error: "Pairing request not found",
|
|
165
|
+
});
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error(`[alphaclaw] Pairing reject error: ${error.message}`);
|
|
168
|
+
res.status(500).json({ ok: false, error: error.message });
|
|
169
|
+
}
|
|
71
170
|
});
|
|
72
171
|
|
|
73
|
-
let devicePairingCache = { pending: [], ts: 0 };
|
|
172
|
+
let devicePairingCache = { pending: [], cliAutoApproveComplete: false, ts: 0 };
|
|
74
173
|
const kDevicePairingCacheTtl = 3000;
|
|
75
174
|
|
|
76
175
|
app.get("/api/devices", async (req, res) => {
|
|
77
|
-
if (!isOnboarded())
|
|
176
|
+
if (!isOnboarded()) {
|
|
177
|
+
return res.json({ pending: [], cliAutoApproveComplete: hasCliAutoApproveMarker() });
|
|
178
|
+
}
|
|
78
179
|
if (Date.now() - devicePairingCache.ts < kDevicePairingCacheTtl) {
|
|
79
|
-
return res.json({
|
|
180
|
+
return res.json({
|
|
181
|
+
pending: devicePairingCache.pending,
|
|
182
|
+
cliAutoApproveComplete: devicePairingCache.cliAutoApproveComplete,
|
|
183
|
+
});
|
|
80
184
|
}
|
|
81
185
|
const result = await clawCmd("devices list --json", { quiet: true });
|
|
82
|
-
if (!result.ok)
|
|
186
|
+
if (!result.ok) {
|
|
187
|
+
return res.json({ pending: [], cliAutoApproveComplete: hasCliAutoApproveMarker() });
|
|
188
|
+
}
|
|
83
189
|
try {
|
|
84
190
|
const parsed = JSON.parse(result.stdout);
|
|
85
191
|
const pendingList = Array.isArray(parsed.pending) ? parsed.pending : [];
|
|
@@ -117,10 +223,11 @@ const registerPairingRoutes = ({ app, clawCmd, isOnboarded, fsModule = fs, openc
|
|
|
117
223
|
scopes: d.scopes || [],
|
|
118
224
|
ts: d.ts || null,
|
|
119
225
|
}));
|
|
120
|
-
|
|
121
|
-
|
|
226
|
+
const cliAutoApproveComplete = hasCliAutoApproveMarker();
|
|
227
|
+
devicePairingCache = { pending, cliAutoApproveComplete, ts: Date.now() };
|
|
228
|
+
res.json({ pending, cliAutoApproveComplete });
|
|
122
229
|
} catch {
|
|
123
|
-
res.json({ pending: [] });
|
|
230
|
+
res.json({ pending: [], cliAutoApproveComplete: hasCliAutoApproveMarker() });
|
|
124
231
|
}
|
|
125
232
|
});
|
|
126
233
|
|
|
@@ -137,4 +244,7 @@ const registerPairingRoutes = ({ app, clawCmd, isOnboarded, fsModule = fs, openc
|
|
|
137
244
|
});
|
|
138
245
|
};
|
|
139
246
|
|
|
140
|
-
module.exports = {
|
|
247
|
+
module.exports = {
|
|
248
|
+
registerPairingRoutes,
|
|
249
|
+
removeAccountRequestsFromPairingStore,
|
|
250
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { buildManagedPaths } = require("../internal-files-migration");
|
|
2
|
+
const { readOpenclawConfig } = require("../openclaw-config");
|
|
2
3
|
|
|
3
4
|
const registerSystemRoutes = ({
|
|
4
5
|
app,
|
|
@@ -23,6 +24,7 @@ const registerSystemRoutes = ({
|
|
|
23
24
|
authProfiles,
|
|
24
25
|
}) => {
|
|
25
26
|
let envRestartPending = false;
|
|
27
|
+
const kManagedChannelTokenPattern = /^(TELEGRAM|DISCORD)_BOT_TOKEN(?:_[A-Z0-9_]+)?$/;
|
|
26
28
|
const kEnvVarsReservedForUserInput = new Set([
|
|
27
29
|
"GITHUB_WORKSPACE_REPO",
|
|
28
30
|
"GOG_KEYRING_PASSWORD",
|
|
@@ -34,6 +36,8 @@ const registerSystemRoutes = ({
|
|
|
34
36
|
const kReservedUserEnvVarKeys = Array.from(
|
|
35
37
|
new Set([...kSystemVars, ...kEnvVarsReservedForUserInput]),
|
|
36
38
|
);
|
|
39
|
+
const isManagedChannelTokenKey = (key) =>
|
|
40
|
+
kManagedChannelTokenPattern.test(String(key || "").trim().toUpperCase());
|
|
37
41
|
const isReservedUserEnvVar = (key) =>
|
|
38
42
|
kSystemVars.has(key) || kEnvVarsReservedForUserInput.has(key);
|
|
39
43
|
const kSystemCronPath = "/etc/cron.d/openclaw-hourly-sync";
|
|
@@ -69,12 +73,73 @@ const registerSystemRoutes = ({
|
|
|
69
73
|
}
|
|
70
74
|
return null;
|
|
71
75
|
};
|
|
72
|
-
const
|
|
76
|
+
const toTitleWords = (value) =>
|
|
77
|
+
String(value || "")
|
|
78
|
+
.trim()
|
|
79
|
+
.split(/[-_\s]+/)
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
82
|
+
.join(" ");
|
|
83
|
+
const hasScopedBindingFields = (match = {}) =>
|
|
84
|
+
!!match.peer ||
|
|
85
|
+
!!match.parentPeer ||
|
|
86
|
+
!!String(match.guildId || "").trim() ||
|
|
87
|
+
!!String(match.teamId || "").trim() ||
|
|
88
|
+
(Array.isArray(match.roles) && match.roles.length > 0);
|
|
89
|
+
const resolveTelegramAccountIdForAgent = ({ config, agentId }) => {
|
|
90
|
+
const normalizedAgentId = String(agentId || "").trim();
|
|
91
|
+
if (!normalizedAgentId) return "default";
|
|
92
|
+
const bindings = Array.isArray(config?.bindings) ? config.bindings : [];
|
|
93
|
+
for (const binding of bindings) {
|
|
94
|
+
if (String(binding?.agentId || "").trim() !== normalizedAgentId) continue;
|
|
95
|
+
const match = binding?.match || {};
|
|
96
|
+
if (String(match.channel || "").trim() !== "telegram") continue;
|
|
97
|
+
if (hasScopedBindingFields(match)) continue;
|
|
98
|
+
return String(match.accountId || "").trim() || "default";
|
|
99
|
+
}
|
|
100
|
+
return normalizedAgentId === "main" ? "default" : "";
|
|
101
|
+
};
|
|
102
|
+
const resolveTelegramChannelNameForAgent = ({ config, agentId }) => {
|
|
103
|
+
const telegramConfig =
|
|
104
|
+
config?.channels?.telegram && typeof config.channels.telegram === "object"
|
|
105
|
+
? config.channels.telegram
|
|
106
|
+
: {};
|
|
107
|
+
const accountId = resolveTelegramAccountIdForAgent({ config, agentId });
|
|
108
|
+
const hasAccounts =
|
|
109
|
+
telegramConfig.accounts && typeof telegramConfig.accounts === "object";
|
|
110
|
+
if (hasAccounts) {
|
|
111
|
+
const accountConfig =
|
|
112
|
+
accountId && telegramConfig.accounts?.[accountId]
|
|
113
|
+
? telegramConfig.accounts[accountId]
|
|
114
|
+
: {};
|
|
115
|
+
const accountName = String(accountConfig?.name || "").trim();
|
|
116
|
+
if (accountName) return accountName;
|
|
117
|
+
} else if (accountId === "default") {
|
|
118
|
+
const legacyName = String(telegramConfig?.name || "").trim();
|
|
119
|
+
if (legacyName) return legacyName;
|
|
120
|
+
}
|
|
121
|
+
return "Telegram";
|
|
122
|
+
};
|
|
123
|
+
const getAgentLabelFromSessionKey = (key = "") => {
|
|
124
|
+
const match = String(key || "").match(/^agent:([^:]+):/);
|
|
125
|
+
const agentId = String(match?.[1] || "").trim();
|
|
126
|
+
if (!agentId) return "Agent";
|
|
127
|
+
if (agentId === "main") return "Main Agent";
|
|
128
|
+
return toTitleWords(agentId);
|
|
129
|
+
};
|
|
130
|
+
const buildSessionLabel = (sessionRow = {}, config = {}) => {
|
|
73
131
|
const key = String(sessionRow?.key || "");
|
|
74
|
-
|
|
132
|
+
const agentLabel = getAgentLabelFromSessionKey(key);
|
|
133
|
+
const agentKeyMatch = key.match(/^agent:([^:]+):/);
|
|
134
|
+
const agentId = String(agentKeyMatch?.[1] || "").trim();
|
|
135
|
+
const telegramChannelName = resolveTelegramChannelNameForAgent({
|
|
136
|
+
config,
|
|
137
|
+
agentId,
|
|
138
|
+
});
|
|
139
|
+
if (key.endsWith(":main")) return `${agentLabel} - Main Thread`;
|
|
75
140
|
const telegramMatch = key.match(/:telegram:direct:([^:]+)$/);
|
|
76
141
|
if (telegramMatch) {
|
|
77
|
-
return
|
|
142
|
+
return `${agentLabel} - Telegram DM (${telegramChannelName})`;
|
|
78
143
|
}
|
|
79
144
|
const telegramTopicMatch = key.match(
|
|
80
145
|
/:telegram:group:([^:]+):topic:([^:]+)$/,
|
|
@@ -89,13 +154,15 @@ const registerSystemRoutes = ({
|
|
|
89
154
|
const topicName = String(
|
|
90
155
|
groupEntry?.topics?.[topicId]?.name || "",
|
|
91
156
|
).trim();
|
|
92
|
-
if (groupName && topicName)
|
|
93
|
-
|
|
94
|
-
|
|
157
|
+
if (groupName && topicName) {
|
|
158
|
+
return `${agentLabel} - ${telegramChannelName} ${groupName} · ${topicName}`;
|
|
159
|
+
}
|
|
160
|
+
if (topicName) return `${agentLabel} - ${telegramChannelName} Topic ${topicName}`;
|
|
161
|
+
return `${agentLabel} - ${telegramChannelName} Topic ${topicId}`;
|
|
95
162
|
}
|
|
96
163
|
const directMatch = key.match(/:direct:([^:]+)$/);
|
|
97
164
|
if (directMatch) {
|
|
98
|
-
return
|
|
165
|
+
return `${agentLabel} - Direct ${directMatch[1]}`;
|
|
99
166
|
}
|
|
100
167
|
return key || "Session";
|
|
101
168
|
};
|
|
@@ -144,12 +211,19 @@ const registerSystemRoutes = ({
|
|
|
144
211
|
};
|
|
145
212
|
|
|
146
213
|
const listSendableAgentSessions = async () => {
|
|
147
|
-
const result = await clawCmd("sessions --json", {
|
|
214
|
+
const result = await clawCmd("sessions --json --all-agents", {
|
|
215
|
+
quiet: true,
|
|
216
|
+
});
|
|
148
217
|
if (!result.ok) {
|
|
149
218
|
throw new Error(result.stderr || "Could not load agent sessions");
|
|
150
219
|
}
|
|
151
220
|
const payload = parseJsonFromStdout(result.stdout);
|
|
152
221
|
const sessions = Array.isArray(payload?.sessions) ? payload.sessions : [];
|
|
222
|
+
const config = readOpenclawConfig({
|
|
223
|
+
fsModule: fs,
|
|
224
|
+
openclawDir: OPENCLAW_DIR,
|
|
225
|
+
fallback: {},
|
|
226
|
+
});
|
|
153
227
|
return sessions
|
|
154
228
|
.filter((sessionRow) => {
|
|
155
229
|
const key = String(sessionRow?.key || "").toLowerCase();
|
|
@@ -170,7 +244,7 @@ const registerSystemRoutes = ({
|
|
|
170
244
|
key,
|
|
171
245
|
sessionId: String(sessionRow?.sessionId || ""),
|
|
172
246
|
updatedAt: Number(sessionRow?.updatedAt) || 0,
|
|
173
|
-
label: buildSessionLabel(sessionRow),
|
|
247
|
+
label: buildSessionLabel(sessionRow, config),
|
|
174
248
|
replyChannel: replyTarget.replyChannel,
|
|
175
249
|
replyTo: replyTarget.replyTo,
|
|
176
250
|
};
|
|
@@ -281,10 +355,15 @@ const registerSystemRoutes = ({
|
|
|
281
355
|
});
|
|
282
356
|
}
|
|
283
357
|
|
|
284
|
-
const filtered = vars.filter(
|
|
358
|
+
const filtered = vars.filter(
|
|
359
|
+
(v) => !isReservedUserEnvVar(v.key) && !isManagedChannelTokenKey(v.key),
|
|
360
|
+
);
|
|
285
361
|
const existingLockedVars = readEnvFile().filter((v) =>
|
|
286
362
|
isReservedUserEnvVar(v.key),
|
|
287
363
|
);
|
|
364
|
+
const existingManagedChannelVars = readEnvFile().filter((v) =>
|
|
365
|
+
isManagedChannelTokenKey(v.key),
|
|
366
|
+
);
|
|
288
367
|
const hiddenKnownVarKeys = new Set(
|
|
289
368
|
kKnownVars
|
|
290
369
|
.filter(
|
|
@@ -298,6 +377,7 @@ const registerSystemRoutes = ({
|
|
|
298
377
|
const nextEnvVars = [
|
|
299
378
|
...filtered,
|
|
300
379
|
...existingHiddenKnownVars,
|
|
380
|
+
...existingManagedChannelVars,
|
|
301
381
|
...existingLockedVars,
|
|
302
382
|
];
|
|
303
383
|
syncChannelConfig(nextEnvVars, "remove");
|