@chrysb/alphaclaw 0.3.3 → 0.3.4
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 +18 -0
- package/lib/plugin/usage-tracker/index.js +308 -0
- package/lib/plugin/usage-tracker/openclaw.plugin.json +8 -0
- package/lib/public/css/explorer.css +51 -1
- package/lib/public/css/shell.css +3 -1
- package/lib/public/css/theme.css +35 -0
- package/lib/public/js/app.js +73 -24
- package/lib/public/js/components/file-tree.js +231 -28
- package/lib/public/js/components/file-viewer.js +193 -20
- package/lib/public/js/components/segmented-control.js +33 -0
- package/lib/public/js/components/sidebar.js +14 -32
- package/lib/public/js/components/telegram-workspace/index.js +353 -0
- package/lib/public/js/components/telegram-workspace/manage.js +397 -0
- package/lib/public/js/components/telegram-workspace/onboarding.js +616 -0
- package/lib/public/js/components/usage-tab.js +528 -0
- package/lib/public/js/components/watchdog-tab.js +1 -1
- package/lib/public/js/lib/api.js +25 -1
- package/lib/public/js/lib/telegram-api.js +78 -0
- package/lib/public/js/lib/ui-settings.js +38 -0
- package/lib/public/setup.html +34 -30
- package/lib/server/alphaclaw-version.js +3 -3
- package/lib/server/constants.js +1 -0
- package/lib/server/onboarding/openclaw.js +15 -0
- package/lib/server/routes/auth.js +5 -1
- package/lib/server/routes/telegram.js +185 -60
- package/lib/server/routes/usage.js +133 -0
- package/lib/server/usage-db.js +570 -0
- package/lib/server.js +21 -1
- package/lib/setup/core-prompts/AGENTS.md +0 -101
- package/package.json +1 -1
- package/lib/public/js/components/telegram-workspace.js +0 -1365
package/lib/public/setup.html
CHANGED
|
@@ -1,33 +1,37 @@
|
|
|
1
|
-
<!
|
|
1
|
+
<!doctype html>
|
|
2
2
|
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>alphaclaw</title>
|
|
7
|
+
<link
|
|
8
|
+
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap"
|
|
9
|
+
rel="stylesheet"
|
|
10
|
+
/>
|
|
11
|
+
<link rel="icon" type="image/svg+xml" href="./img/logo.svg" />
|
|
12
|
+
<link rel="stylesheet" href="./css/theme.css" />
|
|
13
|
+
<link rel="stylesheet" href="./css/shell.css" />
|
|
14
|
+
<link rel="stylesheet" href="./css/explorer.css" />
|
|
15
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
16
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
17
|
+
<script>
|
|
18
|
+
tailwind.config = {
|
|
19
|
+
theme: {
|
|
20
|
+
extend: {
|
|
21
|
+
colors: {
|
|
22
|
+
surface: "var(--bg-sidebar)",
|
|
23
|
+
border: "var(--border)",
|
|
24
|
+
},
|
|
25
|
+
fontFamily: {
|
|
26
|
+
mono: ["'JetBrains Mono'", "monospace"],
|
|
27
|
+
},
|
|
20
28
|
},
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
</
|
|
29
|
-
<body>
|
|
30
|
-
<div id="app"></div>
|
|
31
|
-
<script type="module" src="./js/app.js"></script>
|
|
32
|
-
</body>
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
</script>
|
|
32
|
+
</head>
|
|
33
|
+
<body>
|
|
34
|
+
<div id="app"></div>
|
|
35
|
+
<script type="module" src="./js/app.js"></script>
|
|
36
|
+
</body>
|
|
33
37
|
</html>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
1
|
+
const childProcess = require("child_process");
|
|
2
2
|
const fs = require("fs");
|
|
3
3
|
const path = require("path");
|
|
4
4
|
const https = require("https");
|
|
@@ -140,8 +140,8 @@ const createAlphaclawVersionService = () => {
|
|
|
140
140
|
console.log(
|
|
141
141
|
`[alphaclaw] Running: npm install @chrysb/alphaclaw@latest (cwd: ${installDir})`,
|
|
142
142
|
);
|
|
143
|
-
exec(
|
|
144
|
-
"npm install @chrysb/alphaclaw@latest --omit=dev --no-save --package-lock=false --prefer-online",
|
|
143
|
+
childProcess.exec(
|
|
144
|
+
"npm install @chrysb/alphaclaw@latest --omit=dev --no-save --save=false --package-lock=false --prefer-online",
|
|
145
145
|
{
|
|
146
146
|
cwd: installDir,
|
|
147
147
|
env: {
|
package/lib/server/constants.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
const path = require("path");
|
|
1
2
|
const { buildSecretReplacements } = require("../helpers");
|
|
2
3
|
|
|
4
|
+
const kUsageTrackerPluginPath = path.resolve(
|
|
5
|
+
__dirname,
|
|
6
|
+
"..",
|
|
7
|
+
"..",
|
|
8
|
+
"plugin",
|
|
9
|
+
"usage-tracker",
|
|
10
|
+
);
|
|
11
|
+
|
|
3
12
|
const buildOnboardArgs = ({ varMap, selectedProvider, hasCodexOauth, workspaceDir }) => {
|
|
4
13
|
const onboardArgs = [
|
|
5
14
|
"--non-interactive",
|
|
@@ -116,6 +125,8 @@ const writeSanitizedOpenclawConfig = ({ fs, openclawDir, varMap }) => {
|
|
|
116
125
|
const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
117
126
|
if (!cfg.channels) cfg.channels = {};
|
|
118
127
|
if (!cfg.plugins) cfg.plugins = {};
|
|
128
|
+
if (!cfg.plugins.load) cfg.plugins.load = {};
|
|
129
|
+
if (!Array.isArray(cfg.plugins.load.paths)) cfg.plugins.load.paths = [];
|
|
119
130
|
if (!cfg.plugins.entries) cfg.plugins.entries = {};
|
|
120
131
|
if (!cfg.commands) cfg.commands = {};
|
|
121
132
|
if (!cfg.hooks) cfg.hooks = {};
|
|
@@ -149,6 +160,10 @@ const writeSanitizedOpenclawConfig = ({ fs, openclawDir, varMap }) => {
|
|
|
149
160
|
cfg.plugins.entries.discord = { enabled: true };
|
|
150
161
|
console.log("[onboard] Discord configured");
|
|
151
162
|
}
|
|
163
|
+
if (!cfg.plugins.load.paths.includes(kUsageTrackerPluginPath)) {
|
|
164
|
+
cfg.plugins.load.paths.push(kUsageTrackerPluginPath);
|
|
165
|
+
}
|
|
166
|
+
cfg.plugins.entries["usage-tracker"] = { enabled: true };
|
|
152
167
|
|
|
153
168
|
let content = JSON.stringify(cfg, null, 2);
|
|
154
169
|
const replacements = buildSecretReplacements(varMap, process.env);
|
|
@@ -48,7 +48,11 @@ const registerAuthRoutes = ({ app, loginThrottle }) => {
|
|
|
48
48
|
|
|
49
49
|
const cookieParser = (req) => {
|
|
50
50
|
const cookies = {};
|
|
51
|
-
|
|
51
|
+
const cookieHeader =
|
|
52
|
+
req && req.headers && typeof req.headers.cookie === "string"
|
|
53
|
+
? req.headers.cookie
|
|
54
|
+
: "";
|
|
55
|
+
cookieHeader.split(";").forEach((c) => {
|
|
52
56
|
const [k, ...v] = c.trim().split("=");
|
|
53
57
|
if (k) cookies[k] = v.join("=");
|
|
54
58
|
});
|
|
@@ -19,7 +19,11 @@ const resolveGroupId = (req) => {
|
|
|
19
19
|
const rawGroupId = body.groupId ?? body.chatId;
|
|
20
20
|
return rawGroupId == null ? "" : String(rawGroupId).trim();
|
|
21
21
|
};
|
|
22
|
-
const resolveAllowUserId = async ({
|
|
22
|
+
const resolveAllowUserId = async ({
|
|
23
|
+
telegramApi,
|
|
24
|
+
groupId,
|
|
25
|
+
preferredUserId,
|
|
26
|
+
}) => {
|
|
23
27
|
const normalizedPreferred = String(preferredUserId || "").trim();
|
|
24
28
|
if (normalizedPreferred) return normalizedPreferred;
|
|
25
29
|
const admins = await telegramApi.getChatAdministrators(groupId);
|
|
@@ -43,7 +47,71 @@ const isMissingTopicError = (errorMessage) => {
|
|
|
43
47
|
].some((token) => message.includes(token));
|
|
44
48
|
};
|
|
45
49
|
|
|
46
|
-
const
|
|
50
|
+
const normalizeGitSyncMessagePart = (value) =>
|
|
51
|
+
String(value || "")
|
|
52
|
+
.replace(/[\r\n\t]+/g, " ")
|
|
53
|
+
.replace(/\s+/g, " ")
|
|
54
|
+
.trim();
|
|
55
|
+
|
|
56
|
+
const quoteShellArg = (value) => `'${String(value || "").replace(/'/g, `'\"'\"'`)}'`;
|
|
57
|
+
|
|
58
|
+
const buildTelegramGitSyncCommand = (action, target = "") => {
|
|
59
|
+
const safeAction = normalizeGitSyncMessagePart(action);
|
|
60
|
+
const safeTarget = normalizeGitSyncMessagePart(target);
|
|
61
|
+
const message = `telegram workspace: ${safeAction} ${safeTarget}`.trim();
|
|
62
|
+
return `alphaclaw git-sync -m ${quoteShellArg(message)}`;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const registerTelegramRoutes = ({
|
|
66
|
+
app,
|
|
67
|
+
telegramApi,
|
|
68
|
+
syncPromptFiles,
|
|
69
|
+
shellCmd,
|
|
70
|
+
}) => {
|
|
71
|
+
const repairGroupAllowFromIfMissing = async ({
|
|
72
|
+
cfg,
|
|
73
|
+
groupId,
|
|
74
|
+
requireMention = false,
|
|
75
|
+
}) => {
|
|
76
|
+
const telegramConfig = cfg?.channels?.telegram || {};
|
|
77
|
+
if (
|
|
78
|
+
Array.isArray(telegramConfig.groupAllowFrom) &&
|
|
79
|
+
telegramConfig.groupAllowFrom.length > 0
|
|
80
|
+
) {
|
|
81
|
+
return { repaired: false, resolvedUserId: "", syncWarning: null };
|
|
82
|
+
}
|
|
83
|
+
const resolvedUserId = await resolveAllowUserId({
|
|
84
|
+
telegramApi,
|
|
85
|
+
groupId,
|
|
86
|
+
preferredUserId: "",
|
|
87
|
+
});
|
|
88
|
+
syncConfigForTelegram({
|
|
89
|
+
fs,
|
|
90
|
+
openclawDir: OPENCLAW_DIR,
|
|
91
|
+
topicRegistry,
|
|
92
|
+
groupId,
|
|
93
|
+
requireMention,
|
|
94
|
+
resolvedUserId,
|
|
95
|
+
});
|
|
96
|
+
const syncWarning = await runTelegramGitSync(
|
|
97
|
+
"repair-group-allow-from",
|
|
98
|
+
groupId,
|
|
99
|
+
);
|
|
100
|
+
return { repaired: true, resolvedUserId, syncWarning };
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const runTelegramGitSync = async (action, target = "") => {
|
|
104
|
+
if (typeof shellCmd !== "function") return null;
|
|
105
|
+
try {
|
|
106
|
+
await shellCmd(buildTelegramGitSyncCommand(action, target), {
|
|
107
|
+
timeout: 30000,
|
|
108
|
+
});
|
|
109
|
+
return null;
|
|
110
|
+
} catch (err) {
|
|
111
|
+
return err?.message || "alphaclaw git-sync failed";
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
47
115
|
// Verify bot token
|
|
48
116
|
app.get("/api/telegram/bot", async (req, res) => {
|
|
49
117
|
try {
|
|
@@ -57,7 +125,8 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
|
|
|
57
125
|
// Verify group: checks bot membership, admin rights, topics enabled
|
|
58
126
|
app.post("/api/telegram/groups/verify", async (req, res) => {
|
|
59
127
|
const groupId = resolveGroupId(req);
|
|
60
|
-
if (!groupId)
|
|
128
|
+
if (!groupId)
|
|
129
|
+
return res.status(400).json({ ok: false, error: "groupId is required" });
|
|
61
130
|
|
|
62
131
|
try {
|
|
63
132
|
const chat = await telegramApi.getChat(groupId);
|
|
@@ -69,7 +138,8 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
|
|
|
69
138
|
preferredUserId: "",
|
|
70
139
|
});
|
|
71
140
|
|
|
72
|
-
const isAdmin =
|
|
141
|
+
const isAdmin =
|
|
142
|
+
member.status === "administrator" || member.status === "creator";
|
|
73
143
|
const isForum = !!chat.is_forum;
|
|
74
144
|
|
|
75
145
|
res.json({
|
|
@@ -83,7 +153,7 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
|
|
|
83
153
|
bot: {
|
|
84
154
|
status: member.status,
|
|
85
155
|
isAdmin,
|
|
86
|
-
canManageTopics: isAdmin &&
|
|
156
|
+
canManageTopics: isAdmin && member.can_manage_topics !== false,
|
|
87
157
|
},
|
|
88
158
|
suggestedUserId: suggestedUserId || null,
|
|
89
159
|
});
|
|
@@ -107,9 +177,13 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
|
|
|
107
177
|
const systemInstructions = String(
|
|
108
178
|
body.systemInstructions ?? body.systemPrompt ?? "",
|
|
109
179
|
).trim();
|
|
110
|
-
const iconColorValue =
|
|
111
|
-
|
|
112
|
-
|
|
180
|
+
const iconColorValue =
|
|
181
|
+
rawIconColor == null ? null : Number.parseInt(String(rawIconColor), 10);
|
|
182
|
+
const iconColor = Number.isFinite(iconColorValue)
|
|
183
|
+
? iconColorValue
|
|
184
|
+
: undefined;
|
|
185
|
+
if (!name)
|
|
186
|
+
return res.status(400).json({ ok: false, error: "name is required" });
|
|
113
187
|
|
|
114
188
|
try {
|
|
115
189
|
const result = await telegramApi.createForumTopic(groupId, name, {
|
|
@@ -130,7 +204,16 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
|
|
|
130
204
|
resolvedUserId: "",
|
|
131
205
|
});
|
|
132
206
|
syncPromptFiles();
|
|
133
|
-
|
|
207
|
+
const syncWarning = await runTelegramGitSync("create-topic", result.name);
|
|
208
|
+
res.json({
|
|
209
|
+
ok: true,
|
|
210
|
+
topic: {
|
|
211
|
+
threadId,
|
|
212
|
+
name: result.name,
|
|
213
|
+
iconColor: result.icon_color,
|
|
214
|
+
},
|
|
215
|
+
syncWarning,
|
|
216
|
+
});
|
|
134
217
|
} catch (e) {
|
|
135
218
|
res.json({ ok: false, error: e.message });
|
|
136
219
|
}
|
|
@@ -142,7 +225,9 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
|
|
|
142
225
|
const body = req.body || {};
|
|
143
226
|
const topics = Array.isArray(body.topics) ? body.topics : [];
|
|
144
227
|
if (!Array.isArray(topics) || topics.length === 0) {
|
|
145
|
-
return res
|
|
228
|
+
return res
|
|
229
|
+
.status(400)
|
|
230
|
+
.json({ ok: false, error: "topics array is required" });
|
|
146
231
|
}
|
|
147
232
|
|
|
148
233
|
const results = [];
|
|
@@ -156,7 +241,9 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
|
|
|
156
241
|
iconColor: t.iconColor || undefined,
|
|
157
242
|
});
|
|
158
243
|
const threadId = result.message_thread_id;
|
|
159
|
-
const systemInstructions = String(
|
|
244
|
+
const systemInstructions = String(
|
|
245
|
+
t.systemInstructions ?? t.systemPrompt ?? "",
|
|
246
|
+
).trim();
|
|
160
247
|
topicRegistry.addTopic(groupId, threadId, {
|
|
161
248
|
name: result.name,
|
|
162
249
|
iconColor: result.icon_color,
|
|
@@ -176,64 +263,80 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
|
|
|
176
263
|
resolvedUserId: "",
|
|
177
264
|
});
|
|
178
265
|
syncPromptFiles();
|
|
179
|
-
|
|
266
|
+
const syncWarning = await runTelegramGitSync("bulk-create-topics", groupId);
|
|
267
|
+
res.json({ ok: true, results, syncWarning });
|
|
180
268
|
});
|
|
181
269
|
|
|
182
270
|
// Delete a topic
|
|
183
|
-
app.delete(
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
271
|
+
app.delete(
|
|
272
|
+
"/api/telegram/groups/:groupId/topics/:topicId",
|
|
273
|
+
async (req, res) => {
|
|
274
|
+
const { groupId, topicId } = req.params;
|
|
275
|
+
try {
|
|
276
|
+
await telegramApi.deleteForumTopic(groupId, parseInt(topicId, 10));
|
|
277
|
+
topicRegistry.removeTopic(groupId, topicId);
|
|
278
|
+
syncConfigForTelegram({
|
|
279
|
+
fs,
|
|
280
|
+
openclawDir: OPENCLAW_DIR,
|
|
281
|
+
topicRegistry,
|
|
282
|
+
groupId,
|
|
283
|
+
requireMention: false,
|
|
284
|
+
resolvedUserId: "",
|
|
285
|
+
});
|
|
286
|
+
syncPromptFiles();
|
|
287
|
+
const syncWarning = await runTelegramGitSync("delete-topic", topicId);
|
|
288
|
+
res.json({ ok: true, syncWarning });
|
|
289
|
+
} catch (e) {
|
|
290
|
+
if (!isMissingTopicError(e?.message)) {
|
|
291
|
+
return res.json({ ok: false, error: e.message });
|
|
292
|
+
}
|
|
293
|
+
topicRegistry.removeTopic(groupId, topicId);
|
|
294
|
+
syncConfigForTelegram({
|
|
295
|
+
fs,
|
|
296
|
+
openclawDir: OPENCLAW_DIR,
|
|
297
|
+
topicRegistry,
|
|
298
|
+
groupId,
|
|
299
|
+
requireMention: false,
|
|
300
|
+
resolvedUserId: "",
|
|
301
|
+
});
|
|
302
|
+
syncPromptFiles();
|
|
303
|
+
const syncWarning = await runTelegramGitSync(
|
|
304
|
+
"delete-stale-topic",
|
|
305
|
+
topicId,
|
|
306
|
+
);
|
|
307
|
+
return res.json({
|
|
308
|
+
ok: true,
|
|
309
|
+
removedFromRegistryOnly: true,
|
|
310
|
+
warning:
|
|
311
|
+
"Topic no longer exists in Telegram; removed stale registry entry.",
|
|
312
|
+
syncWarning,
|
|
313
|
+
});
|
|
201
314
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
fs,
|
|
205
|
-
openclawDir: OPENCLAW_DIR,
|
|
206
|
-
topicRegistry,
|
|
207
|
-
groupId,
|
|
208
|
-
requireMention: false,
|
|
209
|
-
resolvedUserId: "",
|
|
210
|
-
});
|
|
211
|
-
syncPromptFiles();
|
|
212
|
-
return res.json({
|
|
213
|
-
ok: true,
|
|
214
|
-
removedFromRegistryOnly: true,
|
|
215
|
-
warning: "Topic no longer exists in Telegram; removed stale registry entry.",
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
});
|
|
315
|
+
},
|
|
316
|
+
);
|
|
219
317
|
|
|
220
318
|
// Rename a topic
|
|
221
319
|
app.put("/api/telegram/groups/:groupId/topics/:topicId", async (req, res) => {
|
|
222
320
|
const { groupId, topicId } = req.params;
|
|
223
321
|
const body = req.body || {};
|
|
224
322
|
const name = String(body.name ?? "").trim();
|
|
225
|
-
const hasSystemInstructions =
|
|
226
|
-
|
|
323
|
+
const hasSystemInstructions =
|
|
324
|
+
Object.prototype.hasOwnProperty.call(body, "systemInstructions") ||
|
|
325
|
+
Object.prototype.hasOwnProperty.call(body, "systemPrompt");
|
|
227
326
|
const systemInstructions = String(
|
|
228
327
|
body.systemInstructions ?? body.systemPrompt ?? "",
|
|
229
328
|
).trim();
|
|
230
|
-
if (!name)
|
|
329
|
+
if (!name)
|
|
330
|
+
return res.status(400).json({ ok: false, error: "name is required" });
|
|
231
331
|
try {
|
|
232
332
|
const threadId = Number.parseInt(String(topicId), 10);
|
|
233
333
|
if (!Number.isFinite(threadId)) {
|
|
234
|
-
return res
|
|
334
|
+
return res
|
|
335
|
+
.status(400)
|
|
336
|
+
.json({ ok: false, error: "topicId must be numeric" });
|
|
235
337
|
}
|
|
236
|
-
const existingTopic =
|
|
338
|
+
const existingTopic =
|
|
339
|
+
topicRegistry.getGroup(groupId)?.topics?.[String(threadId)] || {};
|
|
237
340
|
const existingName = String(existingTopic.name || "").trim();
|
|
238
341
|
const shouldRename = !existingName || existingName !== name;
|
|
239
342
|
if (shouldRename) {
|
|
@@ -260,9 +363,15 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
|
|
|
260
363
|
resolvedUserId: "",
|
|
261
364
|
});
|
|
262
365
|
syncPromptFiles();
|
|
366
|
+
const syncWarning = await runTelegramGitSync("rename-topic", name);
|
|
263
367
|
return res.json({
|
|
264
368
|
ok: true,
|
|
265
|
-
topic: {
|
|
369
|
+
topic: {
|
|
370
|
+
threadId,
|
|
371
|
+
name,
|
|
372
|
+
...(hasSystemInstructions ? { systemInstructions } : {}),
|
|
373
|
+
},
|
|
374
|
+
syncWarning,
|
|
266
375
|
});
|
|
267
376
|
} catch (e) {
|
|
268
377
|
return res.json({ ok: false, error: e.message });
|
|
@@ -296,8 +405,9 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
|
|
|
296
405
|
topicRegistry.setGroup(groupId, { name: groupName });
|
|
297
406
|
syncPromptFiles();
|
|
298
407
|
}
|
|
408
|
+
const syncWarning = await runTelegramGitSync("configure-group", groupId);
|
|
299
409
|
|
|
300
|
-
res.json({ ok: true, userId: resolvedUserId || null });
|
|
410
|
+
res.json({ ok: true, userId: resolvedUserId || null, syncWarning });
|
|
301
411
|
} catch (e) {
|
|
302
412
|
res.json({ ok: false, error: e.message });
|
|
303
413
|
}
|
|
@@ -313,14 +423,24 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
|
|
|
313
423
|
try {
|
|
314
424
|
const debugEnabled = isDebugEnabled();
|
|
315
425
|
const configPath = `${OPENCLAW_DIR}/openclaw.json`;
|
|
316
|
-
|
|
317
|
-
|
|
426
|
+
let cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
427
|
+
let telegramConfig = cfg.channels?.telegram || {};
|
|
318
428
|
const configuredGroups = telegramConfig.groups || {};
|
|
319
429
|
const groupIds = Object.keys(configuredGroups);
|
|
320
430
|
if (groupIds.length === 0) {
|
|
321
431
|
return res.json({ ok: true, configured: false, debugEnabled });
|
|
322
432
|
}
|
|
323
433
|
const groupId = String(groupIds[0]);
|
|
434
|
+
const groupConfig = configuredGroups[groupId] || {};
|
|
435
|
+
const repairResult = await repairGroupAllowFromIfMissing({
|
|
436
|
+
cfg,
|
|
437
|
+
groupId,
|
|
438
|
+
requireMention: !!groupConfig.requireMention,
|
|
439
|
+
});
|
|
440
|
+
if (repairResult.repaired) {
|
|
441
|
+
cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
442
|
+
telegramConfig = cfg.channels?.telegram || {};
|
|
443
|
+
}
|
|
324
444
|
const registryGroup = topicRegistry.getGroup(groupId);
|
|
325
445
|
let groupName = registryGroup?.name || groupId;
|
|
326
446
|
try {
|
|
@@ -336,8 +456,12 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
|
|
|
336
456
|
debugEnabled,
|
|
337
457
|
concurrency: {
|
|
338
458
|
agentMaxConcurrent: cfg.agents?.defaults?.maxConcurrent ?? null,
|
|
339
|
-
subagentMaxConcurrent:
|
|
459
|
+
subagentMaxConcurrent:
|
|
460
|
+
cfg.agents?.defaults?.subagents?.maxConcurrent ?? null,
|
|
340
461
|
},
|
|
462
|
+
repairedGroupAllowFrom: !!repairResult.repaired,
|
|
463
|
+
repairedUserId: repairResult.resolvedUserId || null,
|
|
464
|
+
syncWarning: repairResult.syncWarning || null,
|
|
341
465
|
});
|
|
342
466
|
} catch (e) {
|
|
343
467
|
return res.json({ ok: false, error: e.message });
|
|
@@ -345,7 +469,7 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
|
|
|
345
469
|
});
|
|
346
470
|
|
|
347
471
|
// Reset Telegram workspace onboarding state
|
|
348
|
-
app.post("/api/telegram/workspace/reset", (req, res) => {
|
|
472
|
+
app.post("/api/telegram/workspace/reset", async (req, res) => {
|
|
349
473
|
try {
|
|
350
474
|
const configPath = `${OPENCLAW_DIR}/openclaw.json`;
|
|
351
475
|
const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
@@ -366,11 +490,12 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
|
|
|
366
490
|
}
|
|
367
491
|
|
|
368
492
|
syncPromptFiles();
|
|
369
|
-
|
|
493
|
+
const syncWarning = await runTelegramGitSync("reset-workspace", "telegram");
|
|
494
|
+
return res.json({ ok: true, syncWarning });
|
|
370
495
|
} catch (e) {
|
|
371
496
|
return res.json({ ok: false, error: e.message });
|
|
372
497
|
}
|
|
373
498
|
});
|
|
374
499
|
};
|
|
375
500
|
|
|
376
|
-
module.exports = { registerTelegramRoutes };
|
|
501
|
+
module.exports = { registerTelegramRoutes, buildTelegramGitSyncCommand };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
const topicRegistry = require("../topic-registry");
|
|
2
|
+
|
|
3
|
+
const kSummaryCacheTtlMs = 60 * 1000;
|
|
4
|
+
|
|
5
|
+
const parsePositiveInt = (value, fallbackValue) => {
|
|
6
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
7
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallbackValue;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const createSummaryCache = () => new Map();
|
|
11
|
+
|
|
12
|
+
// Parse "agent:main:telegram:group:-123:topic:42" into structured labels.
|
|
13
|
+
const parseSessionLabels = (sessionKey) => {
|
|
14
|
+
const raw = String(sessionKey || "").trim();
|
|
15
|
+
if (!raw) return null;
|
|
16
|
+
const parts = raw.split(":");
|
|
17
|
+
const labels = [];
|
|
18
|
+
|
|
19
|
+
if (parts[0] === "agent" && parts[1]) {
|
|
20
|
+
labels.push({
|
|
21
|
+
label: parts[1].charAt(0).toUpperCase() + parts[1].slice(1),
|
|
22
|
+
tone: "cyan",
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const channelIndex = parts.indexOf("telegram");
|
|
27
|
+
if (channelIndex !== -1 && parts[channelIndex + 1]) {
|
|
28
|
+
const channelType = parts[channelIndex + 1];
|
|
29
|
+
if (channelType === "direct") {
|
|
30
|
+
labels.push({ label: "Telegram Direct", tone: "blue" });
|
|
31
|
+
} else if (channelType === "group") {
|
|
32
|
+
const groupId = parts[channelIndex + 2] || "";
|
|
33
|
+
let groupName = null;
|
|
34
|
+
let groupEntry = null;
|
|
35
|
+
try {
|
|
36
|
+
groupEntry = topicRegistry.getGroup(groupId);
|
|
37
|
+
groupName = groupEntry?.name || null;
|
|
38
|
+
} catch {}
|
|
39
|
+
labels.push({
|
|
40
|
+
label: groupName || `Group ${groupId}`,
|
|
41
|
+
tone: "purple",
|
|
42
|
+
});
|
|
43
|
+
const topicIndex = parts.indexOf("topic", channelIndex);
|
|
44
|
+
if (topicIndex !== -1 && parts[topicIndex + 1]) {
|
|
45
|
+
const topicId = parts[topicIndex + 1];
|
|
46
|
+
const topicName = groupEntry?.topics?.[topicId]?.name || null;
|
|
47
|
+
labels.push({
|
|
48
|
+
label: topicName || `Topic ${topicId}`,
|
|
49
|
+
tone: "gray",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
labels.push({
|
|
54
|
+
label: `Telegram ${channelType.charAt(0).toUpperCase() + channelType.slice(1)}`,
|
|
55
|
+
tone: "blue",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return labels.length > 0 ? labels : null;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const enrichSessionLabels = (session) => ({
|
|
64
|
+
...session,
|
|
65
|
+
labels: parseSessionLabels(session.sessionKey || session.sessionId),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const registerUsageRoutes = ({
|
|
69
|
+
app,
|
|
70
|
+
requireAuth,
|
|
71
|
+
getDailySummary,
|
|
72
|
+
getSessionsList,
|
|
73
|
+
getSessionDetail,
|
|
74
|
+
getSessionTimeSeries,
|
|
75
|
+
}) => {
|
|
76
|
+
const summaryCache = createSummaryCache();
|
|
77
|
+
|
|
78
|
+
app.get("/api/usage/summary", requireAuth, (req, res) => {
|
|
79
|
+
try {
|
|
80
|
+
const days = parsePositiveInt(req.query.days, 30);
|
|
81
|
+
const cacheKey = String(days);
|
|
82
|
+
const cached = summaryCache.get(cacheKey);
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
if (cached && now - cached.cachedAt <= kSummaryCacheTtlMs) {
|
|
85
|
+
res.json({ ok: true, ...cached.payload, cached: true });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const summary = getDailySummary({ days });
|
|
89
|
+
const payload = { summary };
|
|
90
|
+
summaryCache.set(cacheKey, { payload, cachedAt: now });
|
|
91
|
+
res.json({ ok: true, ...payload, cached: false });
|
|
92
|
+
} catch (err) {
|
|
93
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
app.get("/api/usage/sessions", requireAuth, (req, res) => {
|
|
98
|
+
try {
|
|
99
|
+
const limit = parsePositiveInt(req.query.limit, 50);
|
|
100
|
+
const sessions = getSessionsList({ limit }).map(enrichSessionLabels);
|
|
101
|
+
res.json({ ok: true, sessions });
|
|
102
|
+
} catch (err) {
|
|
103
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
app.get("/api/usage/sessions/:id", requireAuth, (req, res) => {
|
|
108
|
+
try {
|
|
109
|
+
const sessionId = String(req.params.id || "").trim();
|
|
110
|
+
const detail = getSessionDetail({ sessionId });
|
|
111
|
+
if (!detail) {
|
|
112
|
+
res.status(404).json({ ok: false, error: "Session not found" });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
res.json({ ok: true, detail: enrichSessionLabels(detail) });
|
|
116
|
+
} catch (err) {
|
|
117
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
app.get("/api/usage/sessions/:id/timeseries", requireAuth, (req, res) => {
|
|
122
|
+
try {
|
|
123
|
+
const sessionId = String(req.params.id || "").trim();
|
|
124
|
+
const maxPoints = parsePositiveInt(req.query.maxPoints, 100);
|
|
125
|
+
const series = getSessionTimeSeries({ sessionId, maxPoints });
|
|
126
|
+
res.json({ ok: true, series });
|
|
127
|
+
} catch (err) {
|
|
128
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
module.exports = { registerUsageRoutes };
|